1 package html
2
3 import (
4 "bytes"
5 "fmt"
6 "io/ioutil"
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(ioutil.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
103
104
105
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;">"FOO"</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 fmt.Sprintf("</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 }
363
View as plain text