build(markdown): add no_syntax_highlighting tag to build without chroma (#849)

This commit is contained in:
ayn2op 2026-10-18 20:27:14 -04:00 • committed by GitHub
parent cfb31cf2f3
commit 1bcd7650c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 115 additions and 86 deletions

View file

@ -53,7 +53,7 @@ jobs:
- name: Test - name: Test
run: | run: |
go test ${{ matrix.os != 'windows-11-arm' && '-race' || '' }} -v ./... 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 - name: Build
run: go build -trimpath -ldflags=-s . run: go build -trimpath -ldflags=-s .

View file

@ -40,6 +40,7 @@ go build .
- `no_voice` to build without voice support. - `no_voice` to build without voice support.
- `no_spoof_tls_fingerprint` to build without TLS fingerprint spoofing. - `no_spoof_tls_fingerprint` to build without TLS fingerprint spoofing.
- `no_syntax_highlighting` to build without syntax highlighting in code blocks.
## Usage ## Usage

View file

@ -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
}

View file

@ -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)
}

View file

@ -4,14 +4,10 @@ import (
"strconv" "strconv"
"strings" "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" md "github.com/ayn2op/arikawa/v3/markdown"
"github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/tview" "github.com/ayn2op/tview"
"github.com/ayn2op/tview/text" "github.com/ayn2op/tview/text"
"github.com/gdamore/tcell/v3"
"github.com/yuin/goldmark/v2/ast" "github.com/yuin/goldmark/v2/ast"
) )
@ -124,62 +120,17 @@ 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) { func (r *Renderer) renderCodeBlock(builder *Builder, source []byte, node *ast.CodeBlock, base tview.Style) {
var code strings.Builder var code strings.Builder
lines := node.Value.Segments() for _, line := range node.Value.Segments() {
for _, line := range lines {
code.Write(line.Bytes(source)) code.Write(line.Bytes(source))
} }
language, _ := node.Language(source) language, _ := node.Language(source)
language = strings.TrimSpace(language) r.writeCodeBlock(builder, code.String(), strings.TrimSpace(language), base)
lexer := lexers.Get(language) }
declaredLanguageSupported := lexer != nil
// Detect the language from its content. // writeCode writes s in style, starting each line after the first with the code block indent in base.
var analyzed bool func writeCode(builder *Builder, s string, style, base tview.Style) {
if lexer == nil { for i, part := range strings.Split(s, "\n") {
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)
}
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 { if i > 0 {
builder.NewLine() builder.NewLine()
builder.Write(codeBlockIndent, base) builder.Write(codeBlockIndent, base)
@ -188,7 +139,6 @@ func (r *Renderer) renderCodeBlock(builder *Builder, source []byte, node *ast.Co
builder.Write(part, style) builder.Write(part, style)
} }
} }
}
} }
// shouldMask reports whether node is a spoiler shown as "[spoiler]" in place of what it hides. // shouldMask reports whether node is a spoiler shown as "[spoiler]" in place of what it hides.
@ -210,30 +160,6 @@ func listMarker(item *ast.ListItem) string {
return strconv.Itoa(number) + ". " 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 { func mentionText(node *md.Mention) string {
switch { switch {
case node.Channel != nil: case node.Channel != nil: