diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go index dc13253468e..1deea86644b 100644 --- a/gno.land/pkg/gnoweb/app.go +++ b/gno.land/pkg/gnoweb/app.go @@ -7,11 +7,12 @@ import ( "path" "strings" + markdown "github.com/yuin/goldmark-highlighting/v2" + "github.com/alecthomas/chroma/v2" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/styles" "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" - "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/yuin/goldmark" mdhtml "github.com/yuin/goldmark/renderer/html" diff --git a/gno.land/pkg/gnoweb/markdown/highlighting.go b/gno.land/pkg/gnoweb/markdown/highlighting.go deleted file mode 100644 index 51c66674df1..00000000000 --- a/gno.land/pkg/gnoweb/markdown/highlighting.go +++ /dev/null @@ -1,588 +0,0 @@ -// This file was copied from https://github.com/yuin/goldmark-highlighting -// -// package highlighting is an extension for the goldmark(http://github.com/yuin/goldmark). -// -// This extension adds syntax-highlighting to the fenced code blocks using -// chroma(https://github.com/alecthomas/chroma). -package markdown - -import ( - "bytes" - "io" - "strconv" - "strings" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" - "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" - - "github.com/alecthomas/chroma/v2" - chromahtml "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/lexers" - "github.com/alecthomas/chroma/v2/styles" -) - -// ImmutableAttributes is a read-only interface for ast.Attributes. -type ImmutableAttributes interface { - // Get returns (value, true) if an attribute associated with given - // name exists, otherwise (nil, false) - Get(name []byte) (interface{}, bool) - - // GetString returns (value, true) if an attribute associated with given - // name exists, otherwise (nil, false) - GetString(name string) (interface{}, bool) - - // All returns all attributes. - All() []ast.Attribute -} - -type immutableAttributes struct { - n ast.Node -} - -func (a *immutableAttributes) Get(name []byte) (interface{}, bool) { - return a.n.Attribute(name) -} - -func (a *immutableAttributes) GetString(name string) (interface{}, bool) { - return a.n.AttributeString(name) -} - -func (a *immutableAttributes) All() []ast.Attribute { - if a.n.Attributes() == nil { - return []ast.Attribute{} - } - return a.n.Attributes() -} - -// CodeBlockContext holds contextual information of code highlighting. -type CodeBlockContext interface { - // Language returns (language, true) if specified, otherwise (nil, false). - Language() ([]byte, bool) - - // Highlighted returns true if this code block can be highlighted, otherwise false. - Highlighted() bool - - // Attributes return attributes of the code block. - Attributes() ImmutableAttributes -} - -type codeBlockContext struct { - language []byte - highlighted bool - attributes ImmutableAttributes -} - -func newCodeBlockContext(language []byte, highlighted bool, attrs ImmutableAttributes) CodeBlockContext { - return &codeBlockContext{ - language: language, - highlighted: highlighted, - attributes: attrs, - } -} - -func (c *codeBlockContext) Language() ([]byte, bool) { - if c.language != nil { - return c.language, true - } - return nil, false -} - -func (c *codeBlockContext) Highlighted() bool { - return c.highlighted -} - -func (c *codeBlockContext) Attributes() ImmutableAttributes { - return c.attributes -} - -// WrapperRenderer renders wrapper elements like div, pre, etc. -type WrapperRenderer func(w util.BufWriter, context CodeBlockContext, entering bool) - -// CodeBlockOptions creates Chroma options per code block. -type CodeBlockOptions func(ctx CodeBlockContext) []chromahtml.Option - -// Config struct holds options for the extension. -type Config struct { - html.Config - - // Style is a highlighting style. - // Supported styles are defined under https://github.com/alecthomas/chroma/tree/master/formatters. - Style string - - // Pass in a custom Chroma style. If this is not nil, the Style string will be ignored - CustomStyle *chroma.Style - - // If set, will try to guess language if none provided. - // If the guessing fails, we will fall back to a text lexer. - // Note that while Chroma's API supports language guessing, the implementation - // is not there yet, so you will currently always get the basic text lexer. - GuessLanguage bool - - // FormatOptions is a option related to output formats. - // See https://github.com/alecthomas/chroma#the-html-formatter for details. - FormatOptions []chromahtml.Option - - // CSSWriter is an io.Writer that will be used as CSS data output buffer. - // If WithClasses() is enabled, you can get CSS data corresponds to the style. - CSSWriter io.Writer - - // CodeBlockOptions allows set Chroma options per code block. - CodeBlockOptions CodeBlockOptions - - // WrapperRenderer allows you to change wrapper elements. - WrapperRenderer WrapperRenderer -} - -// NewConfig returns a new Config with defaults. -func NewConfig() Config { - return Config{ - Config: html.NewConfig(), - Style: "github", - FormatOptions: []chromahtml.Option{}, - CSSWriter: nil, - WrapperRenderer: nil, - CodeBlockOptions: nil, - } -} - -// SetOption implements renderer.SetOptioner. -func (c *Config) SetOption(name renderer.OptionName, value interface{}) { - switch name { - case optStyle: - c.Style = value.(string) - case optCustomStyle: - c.CustomStyle = value.(*chroma.Style) - case optFormatOptions: - if value != nil { - c.FormatOptions = value.([]chromahtml.Option) - } - case optCSSWriter: - c.CSSWriter = value.(io.Writer) - case optWrapperRenderer: - c.WrapperRenderer = value.(WrapperRenderer) - case optCodeBlockOptions: - c.CodeBlockOptions = value.(CodeBlockOptions) - case optGuessLanguage: - c.GuessLanguage = value.(bool) - default: - c.Config.SetOption(name, value) - } -} - -// Option interface is a functional option interface for the extension. -type Option interface { - renderer.Option - // SetHighlightingOption sets given option to the extension. - SetHighlightingOption(*Config) -} - -type withHTMLOptions struct { - value []html.Option -} - -func (o *withHTMLOptions) SetConfig(c *renderer.Config) { - if o.value != nil { - for _, v := range o.value { - v.(renderer.Option).SetConfig(c) - } - } -} - -func (o *withHTMLOptions) SetHighlightingOption(c *Config) { - if o.value != nil { - for _, v := range o.value { - v.SetHTMLOption(&c.Config) - } - } -} - -// WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options. -func WithHTMLOptions(opts ...html.Option) Option { - return &withHTMLOptions{opts} -} - -const ( - optStyle renderer.OptionName = "HighlightingStyle" - optCustomStyle renderer.OptionName = "HighlightingCustomStyle" -) - -var highlightLinesAttrName = []byte("hl_lines") - -var ( - styleAttrName = []byte("hl_style") - nohlAttrName = []byte("nohl") - linenosAttrName = []byte("linenos") - linenosTableAttrValue = []byte("table") - linenosInlineAttrValue = []byte("inline") - linenostartAttrName = []byte("linenostart") -) - -type withStyle struct { - value string -} - -func (o *withStyle) SetConfig(c *renderer.Config) { - c.Options[optStyle] = o.value -} - -func (o *withStyle) SetHighlightingOption(c *Config) { - c.Style = o.value -} - -// WithStyle is a functional option that changes highlighting style. -func WithStyle(style string) Option { - return &withStyle{style} -} - -type withCustomStyle struct { - value *chroma.Style -} - -func (o *withCustomStyle) SetConfig(c *renderer.Config) { - c.Options[optCustomStyle] = o.value -} - -func (o *withCustomStyle) SetHighlightingOption(c *Config) { - c.CustomStyle = o.value -} - -// WithStyle is a functional option that changes highlighting style. -func WithCustomStyle(style *chroma.Style) Option { - return &withCustomStyle{style} -} - -const optCSSWriter renderer.OptionName = "HighlightingCSSWriter" - -type withCSSWriter struct { - value io.Writer -} - -func (o *withCSSWriter) SetConfig(c *renderer.Config) { - c.Options[optCSSWriter] = o.value -} - -func (o *withCSSWriter) SetHighlightingOption(c *Config) { - c.CSSWriter = o.value -} - -// WithCSSWriter is a functional option that sets io.Writer for CSS data. -func WithCSSWriter(w io.Writer) Option { - return &withCSSWriter{w} -} - -const optGuessLanguage renderer.OptionName = "HighlightingGuessLanguage" - -type withGuessLanguage struct { - value bool -} - -func (o *withGuessLanguage) SetConfig(c *renderer.Config) { - c.Options[optGuessLanguage] = o.value -} - -func (o *withGuessLanguage) SetHighlightingOption(c *Config) { - c.GuessLanguage = o.value -} - -// WithGuessLanguage is a functional option that toggles language guessing -// if none provided. -func WithGuessLanguage(b bool) Option { - return &withGuessLanguage{value: b} -} - -const optWrapperRenderer renderer.OptionName = "HighlightingWrapperRenderer" - -type withWrapperRenderer struct { - value WrapperRenderer -} - -func (o *withWrapperRenderer) SetConfig(c *renderer.Config) { - c.Options[optWrapperRenderer] = o.value -} - -func (o *withWrapperRenderer) SetHighlightingOption(c *Config) { - c.WrapperRenderer = o.value -} - -// WithWrapperRenderer is a functional option that sets WrapperRenderer that -// renders wrapper elements like div, pre, etc. -func WithWrapperRenderer(w WrapperRenderer) Option { - return &withWrapperRenderer{w} -} - -const optCodeBlockOptions renderer.OptionName = "HighlightingCodeBlockOptions" - -type withCodeBlockOptions struct { - value CodeBlockOptions -} - -func (o *withCodeBlockOptions) SetConfig(c *renderer.Config) { - c.Options[optCodeBlockOptions] = o.value -} - -func (o *withCodeBlockOptions) SetHighlightingOption(c *Config) { - c.CodeBlockOptions = o.value -} - -// WithCodeBlockOptions is a functional option that sets CodeBlockOptions that -// allows setting Chroma options per code block. -func WithCodeBlockOptions(c CodeBlockOptions) Option { - return &withCodeBlockOptions{value: c} -} - -const optFormatOptions renderer.OptionName = "HighlightingFormatOptions" - -type withFormatOptions struct { - value []chromahtml.Option -} - -func (o *withFormatOptions) SetConfig(c *renderer.Config) { - if _, ok := c.Options[optFormatOptions]; !ok { - c.Options[optFormatOptions] = []chromahtml.Option{} - } - c.Options[optFormatOptions] = append(c.Options[optFormatOptions].([]chromahtml.Option), o.value...) -} - -func (o *withFormatOptions) SetHighlightingOption(c *Config) { - c.FormatOptions = append(c.FormatOptions, o.value...) -} - -// WithFormatOptions is a functional option that wraps chroma HTML formatter options. -func WithFormatOptions(opts ...chromahtml.Option) Option { - return &withFormatOptions{opts} -} - -// HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension. -type HTMLRenderer struct { - Config -} - -// NewHTMLRenderer builds a new HTMLRenderer with given options and returns it. -func NewHTMLRenderer(opts ...Option) renderer.NodeRenderer { - r := &HTMLRenderer{ - Config: NewConfig(), - } - for _, opt := range opts { - opt.SetHighlightingOption(&r.Config) - } - return r -} - -// RegisterFuncs implements NodeRenderer.RegisterFuncs. -func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) -} - -func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes { - if node.Attributes() != nil { - return &immutableAttributes{node} - } - if infostr != nil { - attrStartIdx := -1 - - for idx, char := range infostr { - if char == '{' { - attrStartIdx = idx - break - } - } - if attrStartIdx > 0 { - n := ast.NewTextBlock() // dummy node for storing attributes - attrStr := infostr[attrStartIdx:] - if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { - for _, attr := range attrs { - n.SetAttribute(attr.Name, attr.Value) - } - return &immutableAttributes{n} - } - } - } - return nil -} - -func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - n := node.(*ast.FencedCodeBlock) - if !entering { - return ast.WalkContinue, nil - } - language := n.Language(source) - - chromaFormatterOptions := make([]chromahtml.Option, 0, len(r.FormatOptions)) - for _, opt := range r.FormatOptions { - chromaFormatterOptions = append(chromaFormatterOptions, opt) - } - - style := r.CustomStyle - if style == nil { - style = styles.Get(r.Style) - } - nohl := false - - var info []byte - if n.Info != nil { - info = n.Info.Segment.Value(source) - } - attrs := getAttributes(n, info) - if attrs != nil { - baseLineNumber := 1 - if linenostartAttr, ok := attrs.Get(linenostartAttrName); ok { - if linenostart, ok := linenostartAttr.(float64); ok { - baseLineNumber = int(linenostart) - chromaFormatterOptions = append( - chromaFormatterOptions, chromahtml.BaseLineNumber(baseLineNumber), - ) - } - } - if linesAttr, hasLinesAttr := attrs.Get(highlightLinesAttrName); hasLinesAttr { - if lines, ok := linesAttr.([]interface{}); ok { - var hlRanges [][2]int - for _, l := range lines { - if ln, ok := l.(float64); ok { - hlRanges = append(hlRanges, [2]int{int(ln) + baseLineNumber - 1, int(ln) + baseLineNumber - 1}) - } - if rng, ok := l.([]uint8); ok { - slices := strings.Split(string(rng), "-") - lhs, err := strconv.Atoi(slices[0]) - if err != nil { - continue - } - rhs := lhs - if len(slices) > 1 { - rhs, err = strconv.Atoi(slices[1]) - if err != nil { - continue - } - } - hlRanges = append(hlRanges, [2]int{lhs + baseLineNumber - 1, rhs + baseLineNumber - 1}) - } - } - chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.HighlightLines(hlRanges)) - } - } - if styleAttr, hasStyleAttr := attrs.Get(styleAttrName); hasStyleAttr { - if st, ok := styleAttr.([]uint8); ok { - styleStr := string(st) - style = styles.Get(styleStr) - } - } - if _, hasNohlAttr := attrs.Get(nohlAttrName); hasNohlAttr { - nohl = true - } - - if linenosAttr, ok := attrs.Get(linenosAttrName); ok { - switch v := linenosAttr.(type) { - case bool: - chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(v)) - case []uint8: - if v != nil { - chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(true)) - } - if bytes.Equal(v, linenosTableAttrValue) { - chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(true)) - } else if bytes.Equal(v, linenosInlineAttrValue) { - chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(false)) - } - } - } - } - - var lexer chroma.Lexer - if language != nil { - lexer = lexers.Get(string(language)) - } - if !nohl && (lexer != nil || r.GuessLanguage) { - if style == nil { - style = styles.Fallback - } - var buffer bytes.Buffer - l := n.Lines().Len() - for i := 0; i < l; i++ { - line := n.Lines().At(i) - buffer.Write(line.Value(source)) - } - - if lexer == nil { - lexer = lexers.Analyse(buffer.String()) - if lexer == nil { - lexer = lexers.Fallback - } - language = []byte(strings.ToLower(lexer.Config().Name)) - } - lexer = chroma.Coalesce(lexer) - - iterator, err := lexer.Tokenise(nil, buffer.String()) - if err == nil { - c := newCodeBlockContext(language, true, attrs) - - if r.CodeBlockOptions != nil { - chromaFormatterOptions = append(chromaFormatterOptions, r.CodeBlockOptions(c)...) - } - formatter := chromahtml.New(chromaFormatterOptions...) - if r.WrapperRenderer != nil { - r.WrapperRenderer(w, c, true) - } - _ = formatter.Format(w, style, iterator) == nil - if r.WrapperRenderer != nil { - r.WrapperRenderer(w, c, false) - } - if r.CSSWriter != nil { - _ = formatter.WriteCSS(r.CSSWriter, style) - } - return ast.WalkContinue, nil - } - } - - var c CodeBlockContext - if r.WrapperRenderer != nil { - c = newCodeBlockContext(language, false, attrs) - r.WrapperRenderer(w, c, true) - } else { - _, _ = w.WriteString("
')
-	}
-	l := n.Lines().Len()
-	for i := 0; i < l; i++ {
-		line := n.Lines().At(i)
-		r.Writer.RawWrite(w, line.Value(source))
-	}
-	if r.WrapperRenderer != nil {
-		r.WrapperRenderer(w, c, false)
-	} else {
-		_, _ = w.WriteString("
\n") - } - return ast.WalkContinue, nil -} - -type highlighting struct { - options []Option -} - -// Highlighting is a goldmark.Extender implementation. -var Highlighting = &highlighting{ - options: []Option{}, -} - -// NewHighlighting returns a new extension with given options. -func NewHighlighting(opts ...Option) goldmark.Extender { - return &highlighting{ - options: opts, - } -} - -// Extend implements goldmark.Extender. -func (e *highlighting) Extend(m goldmark.Markdown) { - m.Renderer().AddOptions(renderer.WithNodeRenderers( - util.Prioritized(NewHTMLRenderer(e.options...), 200), - )) -} diff --git a/gno.land/pkg/gnoweb/markdown/highlighting_test.go b/gno.land/pkg/gnoweb/markdown/highlighting_test.go deleted file mode 100644 index 25bc4fedd61..00000000000 --- a/gno.land/pkg/gnoweb/markdown/highlighting_test.go +++ /dev/null @@ -1,568 +0,0 @@ -// This file was copied from https://github.com/yuin/goldmark-highlighting - -package markdown - -import ( - "bytes" - "fmt" - "strings" - "testing" - - "github.com/alecthomas/chroma/v2" - chromahtml "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/testutil" - "github.com/yuin/goldmark/util" -) - -func TestHighlighting(t *testing.T) { - var css bytes.Buffer - markdown := goldmark.New( - goldmark.WithExtensions( - NewHighlighting( - WithStyle("monokai"), - WithCSSWriter(&css), - WithFormatOptions( - chromahtml.WithClasses(true), - chromahtml.WithLineNumbers(false), - ), - WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { - _, ok := c.Language() - if entering { - if !ok { - w.WriteString("
")
-							return
-						}
-						w.WriteString(`
`) - } else { - if !ok { - w.WriteString("
") - return - } - w.WriteString(``) - } - }), - WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { - if language, ok := c.Language(); ok { - // Turn on line numbers for Go only. - if string(language) == "go" { - return []chromahtml.Option{ - chromahtml.WithLineNumbers(true), - } - } - } - return nil - }), - ), - ), - ) - var buffer bytes.Buffer - if err := markdown.Convert([]byte(` -Title -======= -`+"``` go\n"+`func main() { - fmt.Println("ok") -} -`+"```"+` -`), &buffer); err != nil { - t.Fatal(err) - } - - if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` -

Title

-
1func main() {
-2    fmt.Println("ok")
-3}
-
-`) { - t.Errorf("failed to render HTML\n%s", buffer.String()) - } - - expected := strings.TrimSpace(`/* Background */ .bg { color: #f8f8f2; background-color: #272822; } -/* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; } -/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #f8f8f2; background-color: #3c3d38 } -/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #f8f8f2; background-color: #3c3d38 } -/* Error */ .chroma .err { color: #960050; background-color: #1e0010 } -/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } -/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } -/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } -/* LineHighlight */ .chroma .hl { background-color: #3c3d38 } -/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } -/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } -/* Line */ .chroma .line { display: flex; } -/* Keyword */ .chroma .k { color: #66d9ef } -/* KeywordConstant */ .chroma .kc { color: #66d9ef } -/* KeywordDeclaration */ .chroma .kd { color: #66d9ef } -/* KeywordNamespace */ .chroma .kn { color: #f92672 } -/* KeywordPseudo */ .chroma .kp { color: #66d9ef } -/* KeywordReserved */ .chroma .kr { color: #66d9ef } -/* KeywordType */ .chroma .kt { color: #66d9ef } -/* NameAttribute */ .chroma .na { color: #a6e22e } -/* NameClass */ .chroma .nc { color: #a6e22e } -/* NameConstant */ .chroma .no { color: #66d9ef } -/* NameDecorator */ .chroma .nd { color: #a6e22e } -/* NameException */ .chroma .ne { color: #a6e22e } -/* NameFunction */ .chroma .nf { color: #a6e22e } -/* NameOther */ .chroma .nx { color: #a6e22e } -/* NameTag */ .chroma .nt { color: #f92672 } -/* Literal */ .chroma .l { color: #ae81ff } -/* LiteralDate */ .chroma .ld { color: #e6db74 } -/* LiteralString */ .chroma .s { color: #e6db74 } -/* LiteralStringAffix */ .chroma .sa { color: #e6db74 } -/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } -/* LiteralStringChar */ .chroma .sc { color: #e6db74 } -/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } -/* LiteralStringDoc */ .chroma .sd { color: #e6db74 } -/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } -/* LiteralStringEscape */ .chroma .se { color: #ae81ff } -/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } -/* LiteralStringInterpol */ .chroma .si { color: #e6db74 } -/* LiteralStringOther */ .chroma .sx { color: #e6db74 } -/* LiteralStringRegex */ .chroma .sr { color: #e6db74 } -/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } -/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } -/* LiteralNumber */ .chroma .m { color: #ae81ff } -/* LiteralNumberBin */ .chroma .mb { color: #ae81ff } -/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } -/* LiteralNumberHex */ .chroma .mh { color: #ae81ff } -/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } -/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } -/* LiteralNumberOct */ .chroma .mo { color: #ae81ff } -/* Operator */ .chroma .o { color: #f92672 } -/* OperatorWord */ .chroma .ow { color: #f92672 } -/* Comment */ .chroma .c { color: #75715e } -/* CommentHashbang */ .chroma .ch { color: #75715e } -/* CommentMultiline */ .chroma .cm { color: #75715e } -/* CommentSingle */ .chroma .c1 { color: #75715e } -/* CommentSpecial */ .chroma .cs { color: #75715e } -/* CommentPreproc */ .chroma .cp { color: #75715e } -/* CommentPreprocFile */ .chroma .cpf { color: #75715e } -/* GenericDeleted */ .chroma .gd { color: #f92672 } -/* GenericEmph */ .chroma .ge { font-style: italic } -/* GenericInserted */ .chroma .gi { color: #a6e22e } -/* GenericStrong */ .chroma .gs { font-weight: bold } -/* GenericSubheading */ .chroma .gu { color: #75715e }`) - - gotten := strings.TrimSpace(css.String()) - - if expected != gotten { - diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) - t.Errorf("incorrect CSS.\n%s", string(diff)) - } -} - -func TestHighlighting2(t *testing.T) { - markdown := goldmark.New( - goldmark.WithExtensions( - Highlighting, - ), - ) - var buffer bytes.Buffer - if err := markdown.Convert([]byte(` -Title -======= -`+"```"+` -func main() { - fmt.Println("ok") -} -`+"```"+` -`), &buffer); err != nil { - t.Fatal(err) - } - - if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` -

Title

-
func main() {
-    fmt.Println("ok")
-}
-
-`) { - t.Error("failed to render HTML") - } -} - -func TestHighlighting3(t *testing.T) { - markdown := goldmark.New( - goldmark.WithExtensions( - Highlighting, - ), - ) - var buffer bytes.Buffer - if err := markdown.Convert([]byte(` -Title -======= - -`+"```"+`cpp {hl_lines=[1,2]} -#include -int main() { - std::cout<< "hello" << std::endl; -} -`+"```"+` -`), &buffer); err != nil { - t.Fatal(err) - } - if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` -

Title

-
#include <iostream>
-int main() {
-    std::cout<< "hello" << std::endl;
-}
-
-`) { - t.Errorf("failed to render HTML:\n%s", buffer.String()) - } -} - -func TestHighlightingCustom(t *testing.T) { - custom := chroma.MustNewStyle("custom", chroma.StyleEntries{ - chroma.Background: "#cccccc bg:#1d1d1d", - chroma.Comment: "#999999", - chroma.CommentSpecial: "#cd0000", - chroma.Keyword: "#cc99cd", - chroma.KeywordDeclaration: "#cc99cd", - chroma.KeywordNamespace: "#cc99cd", - chroma.KeywordType: "#cc99cd", - chroma.Operator: "#67cdcc", - chroma.OperatorWord: "#cdcd00", - chroma.NameClass: "#f08d49", - chroma.NameBuiltin: "#f08d49", - chroma.NameFunction: "#f08d49", - chroma.NameException: "bold #666699", - chroma.NameVariable: "#00cdcd", - chroma.LiteralString: "#7ec699", - chroma.LiteralNumber: "#f08d49", - chroma.LiteralStringBoolean: "#f08d49", - chroma.GenericHeading: "bold #000080", - chroma.GenericSubheading: "bold #800080", - chroma.GenericDeleted: "#e2777a", - chroma.GenericInserted: "#cc99cd", - chroma.GenericError: "#e2777a", - chroma.GenericEmph: "italic", - chroma.GenericStrong: "bold", - chroma.GenericPrompt: "bold #000080", - chroma.GenericOutput: "#888", - chroma.GenericTraceback: "#04D", - chroma.GenericUnderline: "underline", - chroma.Error: "border:#e2777a", - }) - - var css bytes.Buffer - markdown := goldmark.New( - goldmark.WithExtensions( - NewHighlighting( - WithStyle("monokai"), // to make sure it is overrided even if present - WithCustomStyle(custom), - WithCSSWriter(&css), - WithFormatOptions( - chromahtml.WithClasses(true), - chromahtml.WithLineNumbers(false), - ), - WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { - _, ok := c.Language() - if entering { - if !ok { - w.WriteString("
")
-							return
-						}
-						w.WriteString(`
`) - } else { - if !ok { - w.WriteString("
") - return - } - w.WriteString(``) - } - }), - WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { - if language, ok := c.Language(); ok { - // Turn on line numbers for Go only. - if string(language) == "go" { - return []chromahtml.Option{ - chromahtml.WithLineNumbers(true), - } - } - } - return nil - }), - ), - ), - ) - var buffer bytes.Buffer - if err := markdown.Convert([]byte(` -Title -======= -`+"``` go\n"+`func main() { - fmt.Println("ok") -} -`+"```"+` -`), &buffer); err != nil { - t.Fatal(err) - } - - if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` -

Title

-
1func main() {
-2    fmt.Println("ok")
-3}
-
-`) { - t.Error("failed to render HTML", buffer.String()) - } - - expected := strings.TrimSpace(`/* Background */ .bg { color: #cccccc; background-color: #1d1d1d; } -/* PreWrapper */ .chroma { color: #cccccc; background-color: #1d1d1d; } -/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #cccccc; background-color: #333333 } -/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #cccccc; background-color: #333333 } -/* Error */ .chroma .err { } -/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } -/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } -/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } -/* LineHighlight */ .chroma .hl { background-color: #333333 } -/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } -/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } -/* Line */ .chroma .line { display: flex; } -/* Keyword */ .chroma .k { color: #cc99cd } -/* KeywordConstant */ .chroma .kc { color: #cc99cd } -/* KeywordDeclaration */ .chroma .kd { color: #cc99cd } -/* KeywordNamespace */ .chroma .kn { color: #cc99cd } -/* KeywordPseudo */ .chroma .kp { color: #cc99cd } -/* KeywordReserved */ .chroma .kr { color: #cc99cd } -/* KeywordType */ .chroma .kt { color: #cc99cd } -/* NameBuiltin */ .chroma .nb { color: #f08d49 } -/* NameClass */ .chroma .nc { color: #f08d49 } -/* NameException */ .chroma .ne { color: #666699; font-weight: bold } -/* NameFunction */ .chroma .nf { color: #f08d49 } -/* NameVariable */ .chroma .nv { color: #00cdcd } -/* LiteralString */ .chroma .s { color: #7ec699 } -/* LiteralStringAffix */ .chroma .sa { color: #7ec699 } -/* LiteralStringBacktick */ .chroma .sb { color: #7ec699 } -/* LiteralStringChar */ .chroma .sc { color: #7ec699 } -/* LiteralStringDelimiter */ .chroma .dl { color: #7ec699 } -/* LiteralStringDoc */ .chroma .sd { color: #7ec699 } -/* LiteralStringDouble */ .chroma .s2 { color: #7ec699 } -/* LiteralStringEscape */ .chroma .se { color: #7ec699 } -/* LiteralStringHeredoc */ .chroma .sh { color: #7ec699 } -/* LiteralStringInterpol */ .chroma .si { color: #7ec699 } -/* LiteralStringOther */ .chroma .sx { color: #7ec699 } -/* LiteralStringRegex */ .chroma .sr { color: #7ec699 } -/* LiteralStringSingle */ .chroma .s1 { color: #7ec699 } -/* LiteralStringSymbol */ .chroma .ss { color: #7ec699 } -/* LiteralNumber */ .chroma .m { color: #f08d49 } -/* LiteralNumberBin */ .chroma .mb { color: #f08d49 } -/* LiteralNumberFloat */ .chroma .mf { color: #f08d49 } -/* LiteralNumberHex */ .chroma .mh { color: #f08d49 } -/* LiteralNumberInteger */ .chroma .mi { color: #f08d49 } -/* LiteralNumberIntegerLong */ .chroma .il { color: #f08d49 } -/* LiteralNumberOct */ .chroma .mo { color: #f08d49 } -/* Operator */ .chroma .o { color: #67cdcc } -/* OperatorWord */ .chroma .ow { color: #cdcd00 } -/* Comment */ .chroma .c { color: #999999 } -/* CommentHashbang */ .chroma .ch { color: #999999 } -/* CommentMultiline */ .chroma .cm { color: #999999 } -/* CommentSingle */ .chroma .c1 { color: #999999 } -/* CommentSpecial */ .chroma .cs { color: #cd0000 } -/* CommentPreproc */ .chroma .cp { color: #999999 } -/* CommentPreprocFile */ .chroma .cpf { color: #999999 } -/* GenericDeleted */ .chroma .gd { color: #e2777a } -/* GenericEmph */ .chroma .ge { font-style: italic } -/* GenericError */ .chroma .gr { color: #e2777a } -/* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold } -/* GenericInserted */ .chroma .gi { color: #cc99cd } -/* GenericOutput */ .chroma .go { color: #888888 } -/* GenericPrompt */ .chroma .gp { color: #000080; font-weight: bold } -/* GenericStrong */ .chroma .gs { font-weight: bold } -/* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold } -/* GenericTraceback */ .chroma .gt { color: #0044dd } -/* GenericUnderline */ .chroma .gl { text-decoration: underline }`) - - gotten := strings.TrimSpace(css.String()) - - if expected != gotten { - diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) - t.Errorf("incorrect CSS.\n%s", string(diff)) - } -} - -func TestHighlightingHlLines(t *testing.T) { - markdown := goldmark.New( - goldmark.WithExtensions( - NewHighlighting( - WithFormatOptions( - chromahtml.WithClasses(true), - ), - ), - ), - ) - - for i, test := range []struct { - attributes string - expect []int - }{ - {`hl_lines=["2"]`, []int{2}}, - {`hl_lines=["2-3",5],linenostart=5`, []int{2, 3, 5}}, - {`hl_lines=["2-3"]`, []int{2, 3}}, - {`hl_lines=["2-3",5],linenostart="5"`, []int{2, 3}}, // linenostart must be a number. string values are ignored - } { - t.Run(fmt.Sprint(i), func(t *testing.T) { - var buffer bytes.Buffer - codeBlock := fmt.Sprintf(`bash {%s} -LINE1 -LINE2 -LINE3 -LINE4 -LINE5 -LINE6 -LINE7 -LINE8 -`, test.attributes) - - if err := markdown.Convert([]byte(` -`+"```"+codeBlock+"```"+` -`), &buffer); err != nil { - t.Fatal(err) - } - - for _, line := range test.expect { - expectStr := fmt.Sprintf("LINE%d\n", line) - if !strings.Contains(buffer.String(), expectStr) { - t.Fatal("got\n", buffer.String(), "\nexpected\n", expectStr) - } - } - }) - } -} - -type nopPreWrapper struct{} - -// Start is called to write a start
 element.
-func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" }
-
-// End is called to write the end 
element. -func (nopPreWrapper) End(code bool) string { return "" } - -func TestHighlightingLinenos(t *testing.T) { - outputLineNumbersInTable := `
- -
-1 - -LINE1 -
-
` - - for i, test := range []struct { - attributes string - lineNumbers bool - lineNumbersInTable bool - expect string - }{ - {`linenos=true`, false, false, `1LINE1 -`}, - {`linenos=false`, false, false, `LINE1 -`}, - {``, true, false, `1LINE1 -`}, - {``, true, true, outputLineNumbersInTable}, - {`linenos=inline`, true, true, `1LINE1 -`}, - {`linenos=foo`, false, false, `1LINE1 -`}, - {`linenos=table`, false, false, outputLineNumbersInTable}, - } { - t.Run(fmt.Sprint(i), func(t *testing.T) { - markdown := goldmark.New( - goldmark.WithExtensions( - NewHighlighting( - WithFormatOptions( - chromahtml.WithLineNumbers(test.lineNumbers), - chromahtml.LineNumbersInTable(test.lineNumbersInTable), - chromahtml.WithPreWrapper(nopPreWrapper{}), - chromahtml.WithClasses(true), - ), - ), - ), - ) - - var buffer bytes.Buffer - codeBlock := fmt.Sprintf(`bash {%s} -LINE1 -`, test.attributes) - - content := "```" + codeBlock + "```" - - if err := markdown.Convert([]byte(content), &buffer); err != nil { - t.Fatal(err) - } - - s := strings.TrimSpace(buffer.String()) - - if s != test.expect { - t.Fatal("got\n", s, "\nexpected\n", test.expect) - } - }) - } -} - -func TestHighlightingGuessLanguage(t *testing.T) { - markdown := goldmark.New( - goldmark.WithExtensions( - NewHighlighting( - WithGuessLanguage(true), - WithFormatOptions( - chromahtml.WithClasses(true), - chromahtml.WithLineNumbers(true), - ), - ), - ), - ) - var buffer bytes.Buffer - if err := markdown.Convert([]byte("```"+` -LINE -`+"```"), &buffer); err != nil { - t.Fatal(err) - } - if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` -
1LINE
-
-`) { - t.Errorf("render mismatch, got\n%s", buffer.String()) - } -} - -func TestCoalesceNeeded(t *testing.T) { - markdown := goldmark.New( - goldmark.WithExtensions( - NewHighlighting( - // WithGuessLanguage(true), - WithFormatOptions( - chromahtml.WithClasses(true), - chromahtml.WithLineNumbers(true), - ), - ), - ), - ) - var buffer bytes.Buffer - if err := markdown.Convert([]byte("```http"+` -GET /foo HTTP/1.1 -Content-Type: application/json -User-Agent: foo - -{ - "hello": "world" -} -`+"```"), &buffer); err != nil { - t.Fatal(err) - } - if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` -
1GET /foo HTTP/1.1
-2Content-Type: application/json
-3User-Agent: foo
-4
-5{
-6  "hello": "world"
-7}
-
-`) { - t.Errorf("render mismatch, got\n%s", buffer.String()) - } -} diff --git a/go.mod b/go.mod index ed7c3b75528..280ca3ae602 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/yuin/goldmark v1.7.2 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.etcd.io/bbolt v1.3.11 go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 diff --git a/go.sum b/go.sum index a4ccfbbdd66..9c4d20dbad6 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,10 @@ dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= @@ -51,6 +53,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -144,8 +148,11 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw=