1. package html
    
  2. 
    
  3. import (
    
  4. 	"bytes"
    
  5. 	"fmt"
    
  6. 	"io"
    
  7. 	"regexp"
    
  8. 	"strings"
    
  9. 	"testing"
    
  10. 
    
  11. 	assert "github.com/alecthomas/assert/v2"
    
  12. 
    
  13. 	"github.com/alecthomas/chroma/v2"
    
  14. 	"github.com/alecthomas/chroma/v2/lexers"
    
  15. 	"github.com/alecthomas/chroma/v2/styles"
    
  16. )
    
  17. 
    
  18. func TestCompressStyle(t *testing.T) {
    
  19. 	style := "color: #888888; background-color: #faffff"
    
  20. 	actual := compressStyle(style)
    
  21. 	expected := "color:#888;background-color:#faffff"
    
  22. 	assert.Equal(t, expected, actual)
    
  23. }
    
  24. 
    
  25. func BenchmarkHTMLFormatter(b *testing.B) {
    
  26. 	formatter := New()
    
  27. 	b.ResetTimer()
    
  28. 	for i := 0; i < b.N; i++ {
    
  29. 		it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
    
  30. 		assert.NoError(b, err)
    
  31. 		err = formatter.Format(io.Discard, styles.Fallback, it)
    
  32. 		assert.NoError(b, err)
    
  33. 	}
    
  34. }
    
  35. 
    
  36. func TestSplitTokensIntoLines(t *testing.T) {
    
  37. 	in := []chroma.Token{
    
  38. 		{Value: "hello", Type: chroma.NameKeyword},
    
  39. 		{Value: " world\nwhat?\n", Type: chroma.NameKeyword},
    
  40. 	}
    
  41. 	expected := [][]chroma.Token{
    
  42. 		{
    
  43. 			{Type: chroma.NameKeyword, Value: "hello"},
    
  44. 			{Type: chroma.NameKeyword, Value: " world\n"},
    
  45. 		},
    
  46. 		{
    
  47. 			{Type: chroma.NameKeyword, Value: "what?\n"},
    
  48. 		},
    
  49. 	}
    
  50. 	actual := chroma.SplitTokensIntoLines(in)
    
  51. 	assert.Equal(t, expected, actual)
    
  52. }
    
  53. 
    
  54. func TestFormatterStyleToCSS(t *testing.T) {
    
  55. 	builder := styles.Get("github").Builder()
    
  56. 	builder.Add(chroma.LineHighlight, "bg:#ffffcc")
    
  57. 	builder.Add(chroma.LineNumbers, "bold")
    
  58. 	style, err := builder.Build()
    
  59. 	if err != nil {
    
  60. 		t.Error(err)
    
  61. 	}
    
  62. 	formatter := New(WithClasses(true))
    
  63. 	css := formatter.styleToCSS(style)
    
  64. 	for _, s := range css {
    
  65. 		if strings.HasPrefix(strings.TrimSpace(s), ";") {
    
  66. 			t.Errorf("rule starts with semicolon - expected valid css rule without semicolon: %v", s)
    
  67. 		}
    
  68. 	}
    
  69. }
    
  70. 
    
  71. func TestClassPrefix(t *testing.T) {
    
  72. 	wantPrefix := "some-prefix-"
    
  73. 	withPrefix := New(WithClasses(true), ClassPrefix(wantPrefix))
    
  74. 	noPrefix := New(WithClasses(true))
    
  75. 	for st := range chroma.StandardTypes {
    
  76. 		if noPrefix.class(st) == "" {
    
  77. 			if got := withPrefix.class(st); got != "" {
    
  78. 				t.Errorf("Formatter.class(%v): prefix shouldn't be added to empty classes", st)
    
  79. 			}
    
  80. 		} else if got := withPrefix.class(st); !strings.HasPrefix(got, wantPrefix) {
    
  81. 			t.Errorf("Formatter.class(%v): %q should have a class prefix", st, got)
    
  82. 		}
    
  83. 	}
    
  84. 
    
  85. 	var styleBuf bytes.Buffer
    
  86. 	err := withPrefix.WriteCSS(&styleBuf, styles.Fallback)
    
  87. 	assert.NoError(t, err)
    
  88. 	if !strings.Contains(styleBuf.String(), ".some-prefix-chroma ") {
    
  89. 		t.Error("Stylesheets should have a class prefix")
    
  90. 	}
    
  91. }
    
  92. 
    
  93. func TestTableLineNumberNewlines(t *testing.T) {
    
  94. 	f := New(WithClasses(true), WithLineNumbers(true), LineNumbersInTable(true))
    
  95. 	it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
    
  96. 	assert.NoError(t, err)
    
  97. 
    
  98. 	var buf bytes.Buffer
    
  99. 	err = f.Format(&buf, styles.Fallback, it)
    
  100. 	assert.NoError(t, err)
    
  101. 
    
  102. 	// Don't bother testing the whole output, just verify it's got line numbers
    
  103. 	// in a <pre>-friendly format.
    
  104. 	// Note: placing the newlines inside the <span> lets browser selections look
    
  105. 	// better, instead of "skipping" over the span margin.
    
  106. 	assert.Contains(t, buf.String(), `<span class="lnt">2
    
  107. </span><span class="lnt">3
    
  108. </span><span class="lnt">4
    
  109. </span>`)
    
  110. }
    
  111. 
    
  112. func TestTabWidthStyle(t *testing.T) {
    
  113. 	f := New(TabWidth(4), WithClasses(false))
    
  114. 	it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
    
  115. 	assert.NoError(t, err)
    
  116. 
    
  117. 	var buf bytes.Buffer
    
  118. 	err = f.Format(&buf, styles.Fallback, it)
    
  119. 	assert.NoError(t, err)
    
  120. 
    
  121. 	assert.True(t, regexp.MustCompile(`<pre.*style=".*background-color:[^;]+;-moz-tab-size:4;-o-tab-size:4;tab-size:4;[^"]*".+`).MatchString(buf.String()))
    
  122. }
    
  123. 
    
  124. func TestWithCustomCSS(t *testing.T) {
    
  125. 	f := New(WithClasses(false), WithCustomCSS(map[chroma.TokenType]string{chroma.Line: `display: inline;`}))
    
  126. 	it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
    
  127. 	assert.NoError(t, err)
    
  128. 
    
  129. 	var buf bytes.Buffer
    
  130. 	err = f.Format(&buf, styles.Fallback, it)
    
  131. 	assert.NoError(t, err)
    
  132. 
    
  133. 	assert.True(t, regexp.MustCompile(`<span style="display:flex;display:inline;"><span><span style=".*">echo</span> FOO</span></span>`).MatchString(buf.String()))
    
  134. }
    
  135. 
    
  136. func TestWithCustomCSSStyleInheritance(t *testing.T) {
    
  137. 	f := New(WithClasses(false), WithCustomCSS(map[chroma.TokenType]string{
    
  138. 		chroma.String:              `background: blue;`,
    
  139. 		chroma.LiteralStringDouble: `color: tomato;`,
    
  140. 	}))
    
  141. 	it, err := lexers.Get("bash").Tokenise(nil, `echo "FOO"`)
    
  142. 	assert.NoError(t, err)
    
  143. 
    
  144. 	var buf bytes.Buffer
    
  145. 	err = f.Format(&buf, styles.Fallback, it)
    
  146. 	assert.NoError(t, err)
    
  147. 
    
  148. 	assert.True(t, regexp.MustCompile(` <span style=".*;background:blue;color:tomato;">&#34;FOO&#34;</span>`).MatchString(buf.String()))
    
  149. }
    
  150. 
    
  151. func TestWrapLongLines(t *testing.T) {
    
  152. 	f := New(WithClasses(false), WrapLongLines(true))
    
  153. 	it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(\"hello world\")\n}\n")
    
  154. 	assert.NoError(t, err)
    
  155. 
    
  156. 	var buf bytes.Buffer
    
  157. 	err = f.Format(&buf, styles.Fallback, it)
    
  158. 	assert.NoError(t, err)
    
  159. 
    
  160. 	assert.True(t, regexp.MustCompile(`<pre.*style=".*white-space:pre-wrap;word-break:break-word;`).MatchString(buf.String()))
    
  161. }
    
  162. 
    
  163. func TestHighlightLines(t *testing.T) {
    
  164. 	f := New(WithClasses(true), HighlightLines([][2]int{{4, 5}}))
    
  165. 	it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(\"hello world\")\n}\n")
    
  166. 	assert.NoError(t, err)
    
  167. 
    
  168. 	var buf bytes.Buffer
    
  169. 	err = f.Format(&buf, styles.Fallback, it)
    
  170. 	assert.NoError(t, err)
    
  171. 
    
  172. 	assert.Contains(t, buf.String(), `<span class="line hl"><span class="cl">`)
    
  173. }
    
  174. 
    
  175. func TestLineNumbers(t *testing.T) {
    
  176. 	f := New(WithClasses(true), WithLineNumbers(true))
    
  177. 	it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
    
  178. 	assert.NoError(t, err)
    
  179. 
    
  180. 	var buf bytes.Buffer
    
  181. 	err = f.Format(&buf, styles.Fallback, it)
    
  182. 	assert.NoError(t, err)
    
  183. 
    
  184. 	assert.Contains(t, buf.String(), `<span class="line"><span class="ln">1</span><span class="cl"><span class="nb">echo</span> FOO</span></span>`)
    
  185. }
    
  186. 
    
  187. func TestPreWrapper(t *testing.T) {
    
  188. 	f := New(Standalone(true), WithClasses(true))
    
  189. 	it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
    
  190. 	assert.NoError(t, err)
    
  191. 
    
  192. 	var buf bytes.Buffer
    
  193. 	err = f.Format(&buf, styles.Fallback, it)
    
  194. 	assert.NoError(t, err)
    
  195. 
    
  196. 	assert.True(t, regexp.MustCompile("<body class=\"bg\">\n<pre.*class=\"chroma\"><code><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> FOO</span></span></code></pre>\n</body>\n</html>").MatchString(buf.String()))
    
  197. 	assert.True(t, regexp.MustCompile(`\.bg { .+ }`).MatchString(buf.String()))
    
  198. 	assert.True(t, regexp.MustCompile(`\.chroma { .+ }`).MatchString(buf.String()))
    
  199. }
    
  200. 
    
  201. func TestLinkeableLineNumbers(t *testing.T) {
    
  202. 	f := New(WithClasses(true), WithLineNumbers(true), WithLinkableLineNumbers(true, "line"), WithClasses(false))
    
  203. 	it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(\"hello world\")\n}\n")
    
  204. 	assert.NoError(t, err)
    
  205. 
    
  206. 	var buf bytes.Buffer
    
  207. 	err = f.Format(&buf, styles.Fallback, it)
    
  208. 	assert.NoError(t, err)
    
  209. 
    
  210. 	assert.Contains(t, buf.String(), `id="line1"><a style="outline:none;text-decoration:none;color:inherit" href="#line1">1</a>`)
    
  211. 	assert.Contains(t, buf.String(), `id="line5"><a style="outline:none;text-decoration:none;color:inherit" href="#line5">5</a>`)
    
  212. }
    
  213. 
    
  214. func TestTableLinkeableLineNumbers(t *testing.T) {
    
  215. 	f := New(Standalone(true), WithClasses(true), WithLineNumbers(true), LineNumbersInTable(true), WithLinkableLineNumbers(true, "line"))
    
  216. 	it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
    
  217. 	assert.NoError(t, err)
    
  218. 
    
  219. 	var buf bytes.Buffer
    
  220. 	err = f.Format(&buf, styles.Fallback, it)
    
  221. 	assert.NoError(t, err)
    
  222. 
    
  223. 	assert.Contains(t, buf.String(), `id="line1"><a class="lnlinks" href="#line1">1</a>`)
    
  224. 	assert.Contains(t, buf.String(), `id="line5"><a class="lnlinks" href="#line5">5</a>`)
    
  225. 	assert.Contains(t, buf.String(), `/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }`, buf.String())
    
  226. }
    
  227. 
    
  228. func TestTableLineNumberSpacing(t *testing.T) {
    
  229. 	testCases := []struct {
    
  230. 		baseLineNumber int
    
  231. 		expectedBuf    string
    
  232. 	}{{
    
  233. 		7,
    
  234. 		`<span class="lnt"> 7
    
  235. </span><span class="lnt"> 8
    
  236. </span><span class="lnt"> 9
    
  237. </span><span class="lnt">10
    
  238. </span><span class="lnt">11
    
  239. </span>`,
    
  240. 	}, {
    
  241. 		6,
    
  242. 		`<span class="lnt"> 6
    
  243. </span><span class="lnt"> 7
    
  244. </span><span class="lnt"> 8
    
  245. </span><span class="lnt"> 9
    
  246. </span><span class="lnt">10
    
  247. </span>`,
    
  248. 	}, {
    
  249. 		5,
    
  250. 		`<span class="lnt">5
    
  251. </span><span class="lnt">6
    
  252. </span><span class="lnt">7
    
  253. </span><span class="lnt">8
    
  254. </span><span class="lnt">9
    
  255. </span>`,
    
  256. 	}}
    
  257. 	for i, testCase := range testCases {
    
  258. 		f := New(
    
  259. 			WithClasses(true),
    
  260. 			WithLineNumbers(true),
    
  261. 			LineNumbersInTable(true),
    
  262. 			BaseLineNumber(testCase.baseLineNumber),
    
  263. 		)
    
  264. 		it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
    
  265. 		assert.NoError(t, err)
    
  266. 		var buf bytes.Buffer
    
  267. 		err = f.Format(&buf, styles.Fallback, it)
    
  268. 		assert.NoError(t, err, "Test Case %d", i)
    
  269. 		assert.Contains(t, buf.String(), testCase.expectedBuf, "Test Case %d", i)
    
  270. 	}
    
  271. }
    
  272. 
    
  273. func TestWithPreWrapper(t *testing.T) {
    
  274. 	wrapper := preWrapper{
    
  275. 		start: func(code bool, styleAttr string) string {
    
  276. 			return fmt.Sprintf("<foo%s id=\"code-%t\">", styleAttr, code)
    
  277. 		},
    
  278. 		end: func(code bool) string {
    
  279. 			return "</foo>"
    
  280. 		},
    
  281. 	}
    
  282. 
    
  283. 	format := func(f *Formatter) string {
    
  284. 		it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
    
  285. 		assert.NoError(t, err)
    
  286. 
    
  287. 		var buf bytes.Buffer
    
  288. 		err = f.Format(&buf, styles.Fallback, it)
    
  289. 		assert.NoError(t, err)
    
  290. 
    
  291. 		return buf.String()
    
  292. 	}
    
  293. 
    
  294. 	t.Run("Regular", func(t *testing.T) {
    
  295. 		s := format(New(WithClasses(true)))
    
  296. 		assert.Equal(t, s, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></code></pre>`)
    
  297. 	})
    
  298. 
    
  299. 	t.Run("PreventSurroundingPre", func(t *testing.T) {
    
  300. 		s := format(New(PreventSurroundingPre(true), WithClasses(true)))
    
  301. 		assert.Equal(t, s, `<span class="nb">echo</span> FOO`)
    
  302. 	})
    
  303. 
    
  304. 	t.Run("InlineCode", func(t *testing.T) {
    
  305. 		s := format(New(InlineCode(true), WithClasses(true)))
    
  306. 		assert.Equal(t, s, `<code class="chroma"><span class="nb">echo</span> FOO</code>`)
    
  307. 	})
    
  308. 
    
  309. 	t.Run("InlineCode, inline styles", func(t *testing.T) {
    
  310. 		s := format(New(InlineCode(true)))
    
  311. 		assert.True(t, regexp.MustCompile(`<code style=".+?"><span style=".+?">echo</span> FOO</code>`).MatchString(s))
    
  312. 	})
    
  313. 
    
  314. 	t.Run("Wrapper", func(t *testing.T) {
    
  315. 		s := format(New(WithPreWrapper(wrapper), WithClasses(true)))
    
  316. 		assert.Equal(t, s, `<foo class="chroma" id="code-true"><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></foo>`)
    
  317. 	})
    
  318. 
    
  319. 	t.Run("Wrapper, LineNumbersInTable", func(t *testing.T) {
    
  320. 		s := format(New(WithPreWrapper(wrapper), WithClasses(true), WithLineNumbers(true), LineNumbersInTable(true)))
    
  321. 
    
  322. 		assert.Equal(t, s, `<div class="chroma">
    
  323. <table class="lntable"><tr><td class="lntd">
    
  324. <foo class="chroma" id="code-false"><span class="lnt">1
    
  325. </span></foo></td>
    
  326. <td class="lntd">
    
  327. <foo class="chroma" id="code-true"><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></foo></td></tr></table>
    
  328. </div>
    
  329. `)
    
  330. 	})
    
  331. }
    
  332. 
    
  333. func TestReconfigureOptions(t *testing.T) {
    
  334. 	options := []Option{
    
  335. 		WithClasses(true),
    
  336. 		WithLineNumbers(true),
    
  337. 	}
    
  338. 
    
  339. 	options = append(options, WithLineNumbers(false))
    
  340. 
    
  341. 	f := New(options...)
    
  342. 
    
  343. 	it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
    
  344. 	assert.NoError(t, err)
    
  345. 
    
  346. 	var buf bytes.Buffer
    
  347. 	err = f.Format(&buf, styles.Fallback, it)
    
  348. 
    
  349. 	assert.NoError(t, err)
    
  350. 	assert.Equal(t, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></code></pre>`, buf.String())
    
  351. }
    
  352. 
    
  353. func TestWriteCssWithAllClasses(t *testing.T) {
    
  354. 	formatter := New()
    
  355. 	formatter.allClasses = true
    
  356. 
    
  357. 	var buf bytes.Buffer
    
  358. 	err := formatter.WriteCSS(&buf, styles.Fallback)
    
  359. 
    
  360. 	assert.NoError(t, err)
    
  361. 	assert.NotContains(t, buf.String(), ".chroma . {", "Generated css doesn't contain invalid css")
    
  362. }