git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/goldmark-highlighting/highlighting_test.go (about)

     1  package highlighting
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/alecthomas/chroma/v2"
    10  	chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
    11  	"github.com/yuin/goldmark"
    12  	"github.com/yuin/goldmark/testutil"
    13  	"github.com/yuin/goldmark/util"
    14  )
    15  
    16  func TestHighlighting(t *testing.T) {
    17  	var css bytes.Buffer
    18  	markdown := goldmark.New(
    19  		goldmark.WithExtensions(
    20  			NewHighlighting(
    21  				WithStyle("monokai"),
    22  				WithCSSWriter(&css),
    23  				WithFormatOptions(
    24  					chromahtml.WithClasses(true),
    25  					chromahtml.WithLineNumbers(false),
    26  				),
    27  				WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) {
    28  					_, ok := c.Language()
    29  					if entering {
    30  						if !ok {
    31  							w.WriteString("<pre><code>")
    32  							return
    33  						}
    34  						w.WriteString(`<div class="highlight">`)
    35  					} else {
    36  						if !ok {
    37  							w.WriteString("</pre></code>")
    38  							return
    39  						}
    40  						w.WriteString(`</div>`)
    41  					}
    42  				}),
    43  				WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option {
    44  					if language, ok := c.Language(); ok {
    45  						// Turn on line numbers for Go only.
    46  						if string(language) == "go" {
    47  							return []chromahtml.Option{
    48  								chromahtml.WithLineNumbers(true),
    49  							}
    50  						}
    51  					}
    52  					return nil
    53  				}),
    54  			),
    55  		),
    56  	)
    57  	var buffer bytes.Buffer
    58  	if err := markdown.Convert([]byte(`
    59  Title
    60  =======
    61  `+"``` go\n"+`func main() {
    62      fmt.Println("ok")
    63  }
    64  `+"```"+`
    65  `), &buffer); err != nil {
    66  		t.Fatal(err)
    67  	}
    68  
    69  	if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
    70  <h1>Title</h1>
    71  <div class="highlight"><pre tabindex="0" class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    72  </span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;ok&#34;</span><span class="p">)</span>
    73  </span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
    74  </span></span></code></pre></div>
    75  `) {
    76  		t.Error("failed to render HTML\n")
    77  	}
    78  
    79  	expected := strings.TrimSpace(`/* Background */ .bg { color: #f8f8f2; background-color: #272822; }
    80  /* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; }
    81  /* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #f8f8f2; background-color: #3c3d38 }
    82  /* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #f8f8f2; background-color: #3c3d38 }
    83  /* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
    84  /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
    85  /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
    86  /* LineHighlight */ .chroma .hl { background-color: #3c3d38 }
    87  /* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
    88  /* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
    89  /* Line */ .chroma .line { display: flex; }
    90  /* Keyword */ .chroma .k { color: #66d9ef }
    91  /* KeywordConstant */ .chroma .kc { color: #66d9ef }
    92  /* KeywordDeclaration */ .chroma .kd { color: #66d9ef }
    93  /* KeywordNamespace */ .chroma .kn { color: #f92672 }
    94  /* KeywordPseudo */ .chroma .kp { color: #66d9ef }
    95  /* KeywordReserved */ .chroma .kr { color: #66d9ef }
    96  /* KeywordType */ .chroma .kt { color: #66d9ef }
    97  /* NameAttribute */ .chroma .na { color: #a6e22e }
    98  /* NameClass */ .chroma .nc { color: #a6e22e }
    99  /* NameConstant */ .chroma .no { color: #66d9ef }
   100  /* NameDecorator */ .chroma .nd { color: #a6e22e }
   101  /* NameException */ .chroma .ne { color: #a6e22e }
   102  /* NameFunction */ .chroma .nf { color: #a6e22e }
   103  /* NameOther */ .chroma .nx { color: #a6e22e }
   104  /* NameTag */ .chroma .nt { color: #f92672 }
   105  /* Literal */ .chroma .l { color: #ae81ff }
   106  /* LiteralDate */ .chroma .ld { color: #e6db74 }
   107  /* LiteralString */ .chroma .s { color: #e6db74 }
   108  /* LiteralStringAffix */ .chroma .sa { color: #e6db74 }
   109  /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 }
   110  /* LiteralStringChar */ .chroma .sc { color: #e6db74 }
   111  /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 }
   112  /* LiteralStringDoc */ .chroma .sd { color: #e6db74 }
   113  /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 }
   114  /* LiteralStringEscape */ .chroma .se { color: #ae81ff }
   115  /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 }
   116  /* LiteralStringInterpol */ .chroma .si { color: #e6db74 }
   117  /* LiteralStringOther */ .chroma .sx { color: #e6db74 }
   118  /* LiteralStringRegex */ .chroma .sr { color: #e6db74 }
   119  /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 }
   120  /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 }
   121  /* LiteralNumber */ .chroma .m { color: #ae81ff }
   122  /* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
   123  /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
   124  /* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
   125  /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
   126  /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
   127  /* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
   128  /* Operator */ .chroma .o { color: #f92672 }
   129  /* OperatorWord */ .chroma .ow { color: #f92672 }
   130  /* Comment */ .chroma .c { color: #75715e }
   131  /* CommentHashbang */ .chroma .ch { color: #75715e }
   132  /* CommentMultiline */ .chroma .cm { color: #75715e }
   133  /* CommentSingle */ .chroma .c1 { color: #75715e }
   134  /* CommentSpecial */ .chroma .cs { color: #75715e }
   135  /* CommentPreproc */ .chroma .cp { color: #75715e }
   136  /* CommentPreprocFile */ .chroma .cpf { color: #75715e }
   137  /* GenericDeleted */ .chroma .gd { color: #f92672 }
   138  /* GenericEmph */ .chroma .ge { font-style: italic }
   139  /* GenericInserted */ .chroma .gi { color: #a6e22e }
   140  /* GenericStrong */ .chroma .gs { font-weight: bold }
   141  /* GenericSubheading */ .chroma .gu { color: #75715e }`)
   142  
   143  	gotten := strings.TrimSpace(css.String())
   144  
   145  	if expected != gotten {
   146  		diff := testutil.DiffPretty([]byte(expected), []byte(gotten))
   147  		t.Errorf("incorrect CSS.\n%s", string(diff))
   148  	}
   149  }
   150  
   151  func TestHighlighting2(t *testing.T) {
   152  	markdown := goldmark.New(
   153  		goldmark.WithExtensions(
   154  			Highlighting,
   155  		),
   156  	)
   157  	var buffer bytes.Buffer
   158  	if err := markdown.Convert([]byte(`
   159  Title
   160  =======
   161  `+"```"+`
   162  func main() {
   163      fmt.Println("ok")
   164  }
   165  `+"```"+`
   166  `), &buffer); err != nil {
   167  		t.Fatal(err)
   168  	}
   169  
   170  	if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
   171  <h1>Title</h1>
   172  <pre><code>func main() {
   173      fmt.Println(&quot;ok&quot;)
   174  }
   175  </code></pre>
   176  `) {
   177  		t.Error("failed to render HTML")
   178  	}
   179  }
   180  
   181  func TestHighlighting3(t *testing.T) {
   182  	markdown := goldmark.New(
   183  		goldmark.WithExtensions(
   184  			Highlighting,
   185  		),
   186  	)
   187  	var buffer bytes.Buffer
   188  	if err := markdown.Convert([]byte(`
   189  Title
   190  =======
   191  
   192  `+"```"+`cpp {hl_lines=[1,2]}
   193  #include <iostream>
   194  int main() {
   195      std::cout<< "hello" << std::endl;
   196  }
   197  `+"```"+`
   198  `), &buffer); err != nil {
   199  		t.Fatal(err)
   200  	}
   201  	if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
   202  <h1>Title</h1>
   203  <pre tabindex="0" style="background-color:#fff;display:grid;"><code><span style="display:flex; background-color:#e5e5e5"><span><span style="color:#999;font-weight:bold;font-style:italic">#include</span> <span style="color:#999;font-weight:bold;font-style:italic">&lt;iostream&gt;</span><span style="color:#999;font-weight:bold;font-style:italic">
   204  </span></span></span><span style="display:flex; background-color:#e5e5e5"><span><span style="color:#999;font-weight:bold;font-style:italic"></span><span style="color:#458;font-weight:bold">int</span> <span style="color:#900;font-weight:bold">main</span>() {
   205  </span></span><span style="display:flex;"><span>    std<span style="color:#000;font-weight:bold">::</span>cout<span style="color:#000;font-weight:bold">&lt;&lt;</span> <span style="color:#d14">&#34;hello&#34;</span> <span style="color:#000;font-weight:bold">&lt;&lt;</span> std<span style="color:#000;font-weight:bold">::</span>endl;
   206  </span></span><span style="display:flex;"><span>}
   207  </span></span></code></pre>
   208  `) {
   209  		t.Error("failed to render HTML")
   210  	}
   211  }
   212  
   213  func TestHighlightingCustom(t *testing.T) {
   214  	custom := chroma.MustNewStyle("custom", chroma.StyleEntries{
   215  		chroma.Background:           "#cccccc bg:#1d1d1d",
   216  		chroma.Comment:              "#999999",
   217  		chroma.CommentSpecial:       "#cd0000",
   218  		chroma.Keyword:              "#cc99cd",
   219  		chroma.KeywordDeclaration:   "#cc99cd",
   220  		chroma.KeywordNamespace:     "#cc99cd",
   221  		chroma.KeywordType:          "#cc99cd",
   222  		chroma.Operator:             "#67cdcc",
   223  		chroma.OperatorWord:         "#cdcd00",
   224  		chroma.NameClass:            "#f08d49",
   225  		chroma.NameBuiltin:          "#f08d49",
   226  		chroma.NameFunction:         "#f08d49",
   227  		chroma.NameException:        "bold #666699",
   228  		chroma.NameVariable:         "#00cdcd",
   229  		chroma.LiteralString:        "#7ec699",
   230  		chroma.LiteralNumber:        "#f08d49",
   231  		chroma.LiteralStringBoolean: "#f08d49",
   232  		chroma.GenericHeading:       "bold #000080",
   233  		chroma.GenericSubheading:    "bold #800080",
   234  		chroma.GenericDeleted:       "#e2777a",
   235  		chroma.GenericInserted:      "#cc99cd",
   236  		chroma.GenericError:         "#e2777a",
   237  		chroma.GenericEmph:          "italic",
   238  		chroma.GenericStrong:        "bold",
   239  		chroma.GenericPrompt:        "bold #000080",
   240  		chroma.GenericOutput:        "#888",
   241  		chroma.GenericTraceback:     "#04D",
   242  		chroma.GenericUnderline:     "underline",
   243  		chroma.Error:                "border:#e2777a",
   244  	})
   245  
   246  	var css bytes.Buffer
   247  	markdown := goldmark.New(
   248  		goldmark.WithExtensions(
   249  			NewHighlighting(
   250  				WithStyle("monokai"), // to make sure it is overrided even if present
   251  				WithCustomStyle(custom),
   252  				WithCSSWriter(&css),
   253  				WithFormatOptions(
   254  					chromahtml.WithClasses(true),
   255  					chromahtml.WithLineNumbers(false),
   256  				),
   257  				WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) {
   258  					_, ok := c.Language()
   259  					if entering {
   260  						if !ok {
   261  							w.WriteString("<pre><code>")
   262  							return
   263  						}
   264  						w.WriteString(`<div class="highlight">`)
   265  					} else {
   266  						if !ok {
   267  							w.WriteString("</pre></code>")
   268  							return
   269  						}
   270  						w.WriteString(`</div>`)
   271  					}
   272  				}),
   273  				WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option {
   274  					if language, ok := c.Language(); ok {
   275  						// Turn on line numbers for Go only.
   276  						if string(language) == "go" {
   277  							return []chromahtml.Option{
   278  								chromahtml.WithLineNumbers(true),
   279  							}
   280  						}
   281  					}
   282  					return nil
   283  				}),
   284  			),
   285  		),
   286  	)
   287  	var buffer bytes.Buffer
   288  	if err := markdown.Convert([]byte(`
   289  Title
   290  =======
   291  `+"``` go\n"+`func main() {
   292      fmt.Println("ok")
   293  }
   294  `+"```"+`
   295  `), &buffer); err != nil {
   296  		t.Fatal(err)
   297  	}
   298  
   299  	if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
   300  <h1>Title</h1>
   301  <div class="highlight"><pre tabindex="0" class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
   302  </span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;ok&#34;</span><span class="p">)</span>
   303  </span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
   304  </span></span></code></pre></div>
   305  `) {
   306  		t.Error("failed to render HTML", buffer.String())
   307  	}
   308  
   309  	expected := strings.TrimSpace(`/* Background */ .bg { color: #cccccc; background-color: #1d1d1d; }
   310  /* PreWrapper */ .chroma { color: #cccccc; background-color: #1d1d1d; }
   311  /* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #cccccc; background-color: #333333 }
   312  /* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #cccccc; background-color: #333333 }
   313  /* Error */ .chroma .err {  }
   314  /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
   315  /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
   316  /* LineHighlight */ .chroma .hl { background-color: #333333 }
   317  /* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 }
   318  /* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 }
   319  /* Line */ .chroma .line { display: flex; }
   320  /* Keyword */ .chroma .k { color: #cc99cd }
   321  /* KeywordConstant */ .chroma .kc { color: #cc99cd }
   322  /* KeywordDeclaration */ .chroma .kd { color: #cc99cd }
   323  /* KeywordNamespace */ .chroma .kn { color: #cc99cd }
   324  /* KeywordPseudo */ .chroma .kp { color: #cc99cd }
   325  /* KeywordReserved */ .chroma .kr { color: #cc99cd }
   326  /* KeywordType */ .chroma .kt { color: #cc99cd }
   327  /* NameBuiltin */ .chroma .nb { color: #f08d49 }
   328  /* NameClass */ .chroma .nc { color: #f08d49 }
   329  /* NameException */ .chroma .ne { color: #666699; font-weight: bold }
   330  /* NameFunction */ .chroma .nf { color: #f08d49 }
   331  /* NameVariable */ .chroma .nv { color: #00cdcd }
   332  /* LiteralString */ .chroma .s { color: #7ec699 }
   333  /* LiteralStringAffix */ .chroma .sa { color: #7ec699 }
   334  /* LiteralStringBacktick */ .chroma .sb { color: #7ec699 }
   335  /* LiteralStringChar */ .chroma .sc { color: #7ec699 }
   336  /* LiteralStringDelimiter */ .chroma .dl { color: #7ec699 }
   337  /* LiteralStringDoc */ .chroma .sd { color: #7ec699 }
   338  /* LiteralStringDouble */ .chroma .s2 { color: #7ec699 }
   339  /* LiteralStringEscape */ .chroma .se { color: #7ec699 }
   340  /* LiteralStringHeredoc */ .chroma .sh { color: #7ec699 }
   341  /* LiteralStringInterpol */ .chroma .si { color: #7ec699 }
   342  /* LiteralStringOther */ .chroma .sx { color: #7ec699 }
   343  /* LiteralStringRegex */ .chroma .sr { color: #7ec699 }
   344  /* LiteralStringSingle */ .chroma .s1 { color: #7ec699 }
   345  /* LiteralStringSymbol */ .chroma .ss { color: #7ec699 }
   346  /* LiteralNumber */ .chroma .m { color: #f08d49 }
   347  /* LiteralNumberBin */ .chroma .mb { color: #f08d49 }
   348  /* LiteralNumberFloat */ .chroma .mf { color: #f08d49 }
   349  /* LiteralNumberHex */ .chroma .mh { color: #f08d49 }
   350  /* LiteralNumberInteger */ .chroma .mi { color: #f08d49 }
   351  /* LiteralNumberIntegerLong */ .chroma .il { color: #f08d49 }
   352  /* LiteralNumberOct */ .chroma .mo { color: #f08d49 }
   353  /* Operator */ .chroma .o { color: #67cdcc }
   354  /* OperatorWord */ .chroma .ow { color: #cdcd00 }
   355  /* Comment */ .chroma .c { color: #999999 }
   356  /* CommentHashbang */ .chroma .ch { color: #999999 }
   357  /* CommentMultiline */ .chroma .cm { color: #999999 }
   358  /* CommentSingle */ .chroma .c1 { color: #999999 }
   359  /* CommentSpecial */ .chroma .cs { color: #cd0000 }
   360  /* CommentPreproc */ .chroma .cp { color: #999999 }
   361  /* CommentPreprocFile */ .chroma .cpf { color: #999999 }
   362  /* GenericDeleted */ .chroma .gd { color: #e2777a }
   363  /* GenericEmph */ .chroma .ge { font-style: italic }
   364  /* GenericError */ .chroma .gr { color: #e2777a }
   365  /* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold }
   366  /* GenericInserted */ .chroma .gi { color: #cc99cd }
   367  /* GenericOutput */ .chroma .go { color: #888888 }
   368  /* GenericPrompt */ .chroma .gp { color: #000080; font-weight: bold }
   369  /* GenericStrong */ .chroma .gs { font-weight: bold }
   370  /* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold }
   371  /* GenericTraceback */ .chroma .gt { color: #0044dd }
   372  /* GenericUnderline */ .chroma .gl { text-decoration: underline }`)
   373  
   374  	gotten := strings.TrimSpace(css.String())
   375  
   376  	if expected != gotten {
   377  		diff := testutil.DiffPretty([]byte(expected), []byte(gotten))
   378  		t.Errorf("incorrect CSS.\n%s", string(diff))
   379  	}
   380  }
   381  
   382  func TestHighlightingHlLines(t *testing.T) {
   383  	markdown := goldmark.New(
   384  		goldmark.WithExtensions(
   385  			NewHighlighting(
   386  				WithFormatOptions(
   387  					chromahtml.WithClasses(true),
   388  				),
   389  			),
   390  		),
   391  	)
   392  
   393  	for i, test := range []struct {
   394  		attributes string
   395  		expect     []int
   396  	}{
   397  		{`hl_lines=["2"]`, []int{2}},
   398  		{`hl_lines=["2-3",5],linenostart=5`, []int{2, 3, 5}},
   399  		{`hl_lines=["2-3"]`, []int{2, 3}},
   400  		{`hl_lines=["2-3",5],linenostart="5"`, []int{2, 3}}, // linenostart must be a number. string values are ignored
   401  	} {
   402  		t.Run(fmt.Sprint(i), func(t *testing.T) {
   403  			var buffer bytes.Buffer
   404  			codeBlock := fmt.Sprintf(`bash {%s}
   405  LINE1
   406  LINE2
   407  LINE3
   408  LINE4
   409  LINE5
   410  LINE6
   411  LINE7
   412  LINE8
   413  `, test.attributes)
   414  
   415  			if err := markdown.Convert([]byte(`
   416  `+"```"+codeBlock+"```"+`
   417  `), &buffer); err != nil {
   418  				t.Fatal(err)
   419  			}
   420  
   421  			for _, line := range test.expect {
   422  				expectStr := fmt.Sprintf("<span class=\"line hl\"><span class=\"cl\">LINE%d\n</span></span>", line)
   423  				if !strings.Contains(buffer.String(), expectStr) {
   424  					t.Fatal("got\n", buffer.String(), "\nexpected\n", expectStr)
   425  				}
   426  			}
   427  		})
   428  	}
   429  }
   430  
   431  type nopPreWrapper struct{}
   432  
   433  // Start is called to write a start <pre> element.
   434  func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" }
   435  
   436  // End is called to write the end </pre> element.
   437  func (nopPreWrapper) End(code bool) string { return "" }
   438  
   439  func TestHighlightingLinenos(t *testing.T) {
   440  	outputLineNumbersInTable := `<div class="chroma">
   441  <table class="lntable"><tr><td class="lntd">
   442  <span class="lnt">1
   443  </span></td>
   444  <td class="lntd">
   445  <span class="line"><span class="cl">LINE1
   446  </span></span></td></tr></table>
   447  </div>`
   448  
   449  	for i, test := range []struct {
   450  		attributes         string
   451  		lineNumbers        bool
   452  		lineNumbersInTable bool
   453  		expect             string
   454  	}{
   455  		{`linenos=true`, false, false, `<span class="line"><span class="ln">1</span><span class="cl">LINE1
   456  </span></span>`},
   457  		{`linenos=false`, false, false, `<span class="line"><span class="cl">LINE1
   458  </span></span>`},
   459  		{``, true, false, `<span class="line"><span class="ln">1</span><span class="cl">LINE1
   460  </span></span>`},
   461  		{``, true, true, outputLineNumbersInTable},
   462  		{`linenos=inline`, true, true, `<span class="line"><span class="ln">1</span><span class="cl">LINE1
   463  </span></span>`},
   464  		{`linenos=foo`, false, false, `<span class="line"><span class="ln">1</span><span class="cl">LINE1
   465  </span></span>`},
   466  		{`linenos=table`, false, false, outputLineNumbersInTable},
   467  	} {
   468  		t.Run(fmt.Sprint(i), func(t *testing.T) {
   469  			markdown := goldmark.New(
   470  				goldmark.WithExtensions(
   471  					NewHighlighting(
   472  						WithFormatOptions(
   473  							chromahtml.WithLineNumbers(test.lineNumbers),
   474  							chromahtml.LineNumbersInTable(test.lineNumbersInTable),
   475  							chromahtml.WithPreWrapper(nopPreWrapper{}),
   476  							chromahtml.WithClasses(true),
   477  						),
   478  					),
   479  				),
   480  			)
   481  
   482  			var buffer bytes.Buffer
   483  			codeBlock := fmt.Sprintf(`bash {%s}
   484  LINE1
   485  `, test.attributes)
   486  
   487  			content := "```" + codeBlock + "```"
   488  
   489  			if err := markdown.Convert([]byte(content), &buffer); err != nil {
   490  				t.Fatal(err)
   491  			}
   492  
   493  			s := strings.TrimSpace(buffer.String())
   494  
   495  			if s != test.expect {
   496  				t.Fatal("got\n", s, "\nexpected\n", test.expect)
   497  			}
   498  		})
   499  	}
   500  }
   501  
   502  func TestHighlightingGuessLanguage(t *testing.T) {
   503  	markdown := goldmark.New(
   504  		goldmark.WithExtensions(
   505  			NewHighlighting(
   506  				WithGuessLanguage(true),
   507  				WithFormatOptions(
   508  					chromahtml.WithClasses(true),
   509  					chromahtml.WithLineNumbers(true),
   510  				),
   511  			),
   512  		),
   513  	)
   514  	var buffer bytes.Buffer
   515  	if err := markdown.Convert([]byte("```"+`
   516  LINE	
   517  `+"```"), &buffer); err != nil {
   518  		t.Fatal(err)
   519  	}
   520  	if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
   521  <pre tabindex="0" class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">LINE	
   522  </span></span></code></pre>
   523  `) {
   524  		t.Errorf("render mismatch, got\n%s", buffer.String())
   525  	}
   526  }
   527  
   528  func TestCoalesceNeeded(t *testing.T) {
   529  	markdown := goldmark.New(
   530  		goldmark.WithExtensions(
   531  			NewHighlighting(
   532  				// WithGuessLanguage(true),
   533  				WithFormatOptions(
   534  					chromahtml.WithClasses(true),
   535  					chromahtml.WithLineNumbers(true),
   536  				),
   537  			),
   538  		),
   539  	)
   540  	var buffer bytes.Buffer
   541  	if err := markdown.Convert([]byte("```http"+`
   542  GET /foo HTTP/1.1
   543  Content-Type: application/json
   544  User-Agent: foo
   545  
   546  {
   547    "hello": "world"
   548  }
   549  `+"```"), &buffer); err != nil {
   550  		t.Fatal(err)
   551  	}
   552  	if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
   553  <pre tabindex="0" class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="nf">GET</span> <span class="nn">/foo</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
   554  </span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
   555  </span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">User-Agent</span><span class="o">:</span> <span class="l">foo</span>
   556  </span></span><span class="line"><span class="ln">4</span><span class="cl">
   557  </span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">{</span>
   558  </span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nt">&#34;hello&#34;</span><span class="p">:</span> <span class="s2">&#34;world&#34;</span>
   559  </span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span>
   560  </span></span></code></pre>
   561  `) {
   562  		t.Errorf("render mismatch, got\n%s", buffer.String())
   563  	}
   564  }