From 1bcd7650c8ff72fa6ada7c46f2f4545ae50648a4 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Sun, 18 Oct 2026 20:27:14 -0400 Subject: [PATCH] build(markdown): add no_syntax_highlighting tag to build without chroma (#849) --- .github/workflows/ci.yml | 2 +- README.md | 1 + internal/markdown/highlight.go | 85 +++++++++++++++++++++++++ internal/markdown/highlight_none.go | 17 +++++ internal/markdown/renderer.go | 96 ++++------------------------- 5 files changed, 115 insertions(+), 86 deletions(-) create mode 100644 internal/markdown/highlight.go create mode 100644 internal/markdown/highlight_none.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9f524a..a3b5486 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - name: Test run: | go test ${{ matrix.os != 'windows-11-arm' && '-race' || '' }} -v ./... - go test ${{ matrix.os != 'windows-11-arm' && '-race' || '' }} -v -tags no_spoof_tls_fingerprint,no_voice ./... + go test ${{ matrix.os != 'windows-11-arm' && '-race' || '' }} -v -tags no_spoof_tls_fingerprint,no_voice,no_syntax_highlighting ./... - name: Build run: go build -trimpath -ldflags=-s . diff --git a/README.md b/README.md index 1bf35d7..8797b7a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ go build . - `no_voice` to build without voice support. - `no_spoof_tls_fingerprint` to build without TLS fingerprint spoofing. +- `no_syntax_highlighting` to build without syntax highlighting in code blocks. ## Usage diff --git a/internal/markdown/highlight.go b/internal/markdown/highlight.go new file mode 100644 index 0000000..29cfb9e --- /dev/null +++ b/internal/markdown/highlight.go @@ -0,0 +1,85 @@ +//go:build !no_syntax_highlighting + +package markdown + +import ( + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/alecthomas/chroma/v2/styles" + "github.com/ayn2op/tview" + "github.com/gdamore/tcell/v3" +) + +func (r *Renderer) writeCodeBlock(builder *Builder, code string, language string, base tview.Style) { + lexer := lexers.Get(language) + declaredLanguageSupported := lexer != nil + + // Detect the language from its content. + var analyzed bool + if lexer == nil { + lexer = lexers.Analyse(code) + analyzed = lexer != nil + } + if lexer == nil { + lexer = lexers.Fallback + } + + // Some lexers emit many tiny tokens; coalesce runs of the same type. + lexer = chroma.Coalesce(lexer) + + var header string + switch { + case analyzed: + header = "code: analyzed" + case language == "": + header = "code" + case !declaredLanguageSupported: + header = "code: " + language + } + if header != "" { + builder.Write(codeBlockIndent+header, base.Dim(true)) + builder.NewLine() + } + + iterator, err := lexer.Tokenise(nil, code) + if err != nil { + builder.Write(codeBlockIndent, base) + writeCode(builder, code, base, base) + return + } + + theme := styles.Get(r.cfg.Markdown.Theme) + if theme == nil { + theme = styles.Fallback + } + + builder.Write(codeBlockIndent, base) + for token := iterator(); token != chroma.EOF; token = iterator() { + style := applyChromaStyle(base, theme.Get(token.Type)) + writeCode(builder, token.Value, style, base) + } +} + +func applyChromaStyle(base tview.Style, entry chroma.StyleEntry) tview.Style { + style := base + if entry.Colour.IsSet() { + style = style.Foreground(tcell.NewRGBColor( + int32(entry.Colour.Red()), + int32(entry.Colour.Green()), + int32(entry.Colour.Blue()), + )) + } + // Token background colors are not applied, so code blocks keep the user's terminal/chat background. + + // An attribute chroma passes on is left as it is. + if entry.Bold != chroma.Pass { + style = style.Bold(entry.Bold == chroma.Yes) + } + if entry.Italic != chroma.Pass { + style = style.Italic(entry.Italic == chroma.Yes) + } + if entry.Underline != chroma.Pass { + style = style.Underline(entry.Underline == chroma.Yes) + } + return style +} diff --git a/internal/markdown/highlight_none.go b/internal/markdown/highlight_none.go new file mode 100644 index 0000000..f964b99 --- /dev/null +++ b/internal/markdown/highlight_none.go @@ -0,0 +1,17 @@ +//go:build no_syntax_highlighting + +package markdown + +import "github.com/ayn2op/tview" + +func (r *Renderer) writeCodeBlock(builder *Builder, code string, language string, base tview.Style) { + header := "code" + if language != "" { + header += ": " + language + } + builder.Write(codeBlockIndent+header, base.Dim(true)) + builder.NewLine() + + builder.Write(codeBlockIndent, base) + writeCode(builder, code, base, base) +} diff --git a/internal/markdown/renderer.go b/internal/markdown/renderer.go index cef927b..41a8fe0 100644 --- a/internal/markdown/renderer.go +++ b/internal/markdown/renderer.go @@ -4,14 +4,10 @@ import ( "strconv" "strings" - "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/lexers" - "github.com/alecthomas/chroma/v2/styles" md "github.com/ayn2op/arikawa/v3/markdown" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/tview" "github.com/ayn2op/tview/text" - "github.com/gdamore/tcell/v3" "github.com/yuin/goldmark/v2/ast" ) @@ -124,69 +120,23 @@ func (r *Renderer) RenderText(source []byte, node ast.Node, base tview.Style, em func (r *Renderer) renderCodeBlock(builder *Builder, source []byte, node *ast.CodeBlock, base tview.Style) { var code strings.Builder - lines := node.Value.Segments() - for _, line := range lines { + for _, line := range node.Value.Segments() { code.Write(line.Bytes(source)) } language, _ := node.Language(source) - language = strings.TrimSpace(language) - lexer := lexers.Get(language) - declaredLanguageSupported := lexer != nil + r.writeCodeBlock(builder, code.String(), strings.TrimSpace(language), base) +} - // Detect the language from its content. - var analyzed bool - if lexer == nil { - lexer = lexers.Analyse(code.String()) - analyzed = lexer != nil - } - if lexer == nil { - lexer = lexers.Fallback - } - - // Some lexers emit many tiny tokens; coalesce runs of the same type. - lexer = chroma.Coalesce(lexer) - - var header string - switch { - case analyzed: - header = "code: analyzed" - case language == "": - header = "code" - case !declaredLanguageSupported: - header = "code: " + language - } - if header != "" { - builder.Write(codeBlockIndent+header, base.Dim(true)) - builder.NewLine() - } - - iterator, err := lexer.Tokenise(nil, code.String()) - if err != nil { - for _, line := range lines { - builder.Write(codeBlockIndent+string(line.Bytes(source)), base) +// writeCode writes s in style, starting each line after the first with the code block indent in base. +func writeCode(builder *Builder, s string, style, base tview.Style) { + for i, part := range strings.Split(s, "\n") { + if i > 0 { + builder.NewLine() + builder.Write(codeBlockIndent, base) } - return - } - - theme := styles.Get(r.cfg.Markdown.Theme) - if theme == nil { - theme = styles.Fallback - } - - builder.Write(codeBlockIndent, base) - for token := iterator(); token != chroma.EOF; token = iterator() { - style := applyChromaStyle(base, theme.Get(token.Type)) - // Chroma tokens may include embedded newlines, so split and re-emit with indentation on each visual line. - parts := strings.Split(token.Value, "\n") - for i, part := range parts { - if i > 0 { - builder.NewLine() - builder.Write(codeBlockIndent, base) - } - if part != "" { - builder.Write(part, style) - } + if part != "" { + builder.Write(part, style) } } } @@ -210,30 +160,6 @@ func listMarker(item *ast.ListItem) string { return strconv.Itoa(number) + ". " } -func applyChromaStyle(base tview.Style, entry chroma.StyleEntry) tview.Style { - style := base - if entry.Colour.IsSet() { - style = style.Foreground(tcell.NewRGBColor( - int32(entry.Colour.Red()), - int32(entry.Colour.Green()), - int32(entry.Colour.Blue()), - )) - } - // Token background colors are not applied, so code blocks keep the user's terminal/chat background. - - // An attribute chroma passes on is left as it is. - if entry.Bold != chroma.Pass { - style = style.Bold(entry.Bold == chroma.Yes) - } - if entry.Italic != chroma.Pass { - style = style.Italic(entry.Italic == chroma.Yes) - } - if entry.Underline != chroma.Pass { - style = style.Underline(entry.Underline == chroma.Yes) - } - return style -} - func mentionText(node *md.Mention) string { switch { case node.Channel != nil: