Files
librenotes/internal/notesium/highlight.go
T
libretechandClaude Opus 4.7 094250609c Fork Notesium source and restructure into Go package layout
Initial fork of github.com/alonswartz/notesium into librenotes:
- Source moved to internal/notesium/ (package notesium)
- Thin entry point at cmd/librenotes/main.go
- Module renamed to git.librete.ch/public/librenotes
- main() exposed as notesium.Run()
- LICENSE preserved (MIT), NOTICE added with attribution
- Web assets and completion.bash co-located with embedding code
  to satisfy go:embed path constraints

Closes #3, #34, #35.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:52:25 +02:00

176 lines
4.6 KiB
Go

package notesium
import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strings"
)
const (
ansiHeading = "\033[1m"
ansiBold = "\033[1m"
ansiItalic = "\033[3m"
ansiLink = "\033[34m"
ansiCodeBlock = "\033[33m"
ansiInlineCode = "\033[33m"
ansiBlockQuote = "\033[36m"
ansiListMarker = "\033[36m"
ansiReset = "\033[0m"
)
var ansiLineBg = func() string {
switch os.Getenv("NOTESIUM_FINDER_THEME") {
case "light":
return "\033[48;5;7m"
default:
return "\033[40m"
}
}()
var (
reBold = regexp.MustCompile(`\*\*(.*?)\*\*`)
reBoldAlt = regexp.MustCompile(`__(.*?)__`)
reItalic = regexp.MustCompile(`\*(.*?)\*`)
reItalicAlt = regexp.MustCompile(`_(.*?)_`)
reUnorderedList = regexp.MustCompile(`^(\s*[-+*]) `)
reOrderedList = regexp.MustCompile(`^(\s*\d+\.) `)
reInlineCode = regexp.MustCompile("`(.*?)`")
reLinkPlain = regexp.MustCompile(`(?:https?://|www\.)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/[^\s]*)?`)
reLinkMarkdown = regexp.MustCompile(`\[(.[^]]*?)\]\((.[^)]*?)\)`)
reAnsi = regexp.MustCompile(`\x1b\[[0-9;]*m`)
reReset = regexp.MustCompile(`\x1b\[0m`)
)
func renderMarkdown(reader io.Reader, writer io.Writer, lineNumber int) {
inCodeBlock := false
scanner := bufio.NewScanner(reader)
for lineIndex := 1; scanner.Scan(); lineIndex++ {
line := scanner.Text()
highlightedLine := highlightLine(line, &inCodeBlock)
if lineNumber > 0 && lineNumber == lineIndex {
highlightedLine = highlightLineWithBackground(highlightedLine)
}
fmt.Fprintln(writer, highlightedLine)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(writer, "Error reading content: %v\n", err)
}
}
func highlightLine(line string, inCodeBlock *bool) string {
// Code blocks
if strings.HasPrefix(line, "```") {
*inCodeBlock = !*inCodeBlock
return ansiCodeBlock + line + ansiReset
}
if *inCodeBlock {
return ansiCodeBlock + line + ansiReset
}
// Headers
if strings.HasPrefix(line, "#") {
return ansiHeading + line + ansiReset
}
// Blockquotes
if strings.HasPrefix(line, "> ") {
return ansiBlockQuote + line + ansiReset
}
// Inline code
matches := reInlineCode.FindAllStringSubmatchIndex(line, -1)
if len(matches) > 0 {
return highlightLineWithInlineCode(line, matches)
}
return highlightString(line)
}
func highlightString(line string) string {
// Links
line = highlightLink(line, reLinkMarkdown, ansiLink)
line = highlightRegex(line, reLinkPlain, ansiLink, 0)
// Bold (**text** or __text__)
line = highlightRegex(line, reBold, ansiBold, 2)
line = highlightRegex(line, reBoldAlt, ansiBold, 2)
// Italic (*text* or _text_)
line = highlightRegex(line, reItalic, ansiItalic, 1)
line = highlightRegex(line, reItalicAlt, ansiItalic, 1)
// List markers
line = highlightRegex(line, reUnorderedList, ansiListMarker, 0)
line = highlightRegex(line, reOrderedList, ansiListMarker, 0)
return line
}
func highlightRegex(line string, re *regexp.Regexp, ansiCode string, markerLength int) string {
return re.ReplaceAllStringFunc(line, func(match string) string {
inner := match[markerLength : len(match)-markerLength]
return ansiCode + inner + ansiReset
})
}
func highlightLink(line string, re *regexp.Regexp, ansiCode string) string {
return re.ReplaceAllStringFunc(line, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) >= 2 {
title := matches[1]
return ansiCode + title + ansiReset
}
return match
})
}
func highlightLineWithInlineCode(line string, matches [][]int) string {
var builder strings.Builder
prevIndex := 0
for _, match := range matches {
start, end := match[0], match[1]
groupStart, groupEnd := match[2], match[3]
// Handle text before inline code
if start > prevIndex {
builder.WriteString(highlightString(line[prevIndex:start]))
}
builder.WriteString(ansiInlineCode)
builder.WriteString(line[groupStart:groupEnd])
builder.WriteString(ansiReset)
prevIndex = end
}
// Handle text after inline code
if prevIndex < len(line) {
builder.WriteString(highlightString(line[prevIndex:]))
}
return builder.String()
}
func highlightLineWithBackground(highlightedLine string) string {
// apply bg after resets to handle segments
highlightedLine = reReset.ReplaceAllStringFunc(highlightedLine, func(reset string) string {
return reset + ansiLineBg
})
// apply padding
termWidth := 79
visibleChars := len(reAnsi.ReplaceAllString(highlightedLine, ""))
requiredPadding := termWidth - visibleChars
if requiredPadding > 0 {
padding := strings.Repeat(" ", requiredPadding)
highlightedLine += ansiLineBg + padding
}
return ansiLineBg + highlightedLine + ansiReset
}