Skip to content

Commit a6d00fe

Browse files
alecthomasclaude
andcommitted
fix(html): make mode class output opt-in via WithModeClasses
Fixes #1287 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f52d015 commit a6d00fe

3 files changed

Lines changed: 97 additions & 85 deletions

File tree

formatters/html/html.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ func WithPreWrapper(wrapper PreWrapper) Option {
8484
}
8585
}
8686

87+
// WithModeClasses adds the style's mode (eg. "light" or "dark") as a CSS
88+
// class on wrapper elements and scopes WriteCSS rules by mode. This enables
89+
// combining light and dark stylesheets and switching themes at runtime.
90+
func WithModeClasses(b bool) Option { return func(f *Formatter) { f.modeClasses = b } }
91+
8792
// WrapLongLines wraps long lines.
8893
func WrapLongLines(b bool) Option {
8994
return func(f *Formatter) {
@@ -207,6 +212,7 @@ type Formatter struct {
207212
inlineCode bool
208213
preventSurroundingPre bool
209214
tabWidth int
215+
modeClasses bool
210216
wrapLongLines bool
211217
lineNumbers bool
212218
lineNumbersInTable bool
@@ -425,7 +431,7 @@ func (f *Formatter) modeClass(style *chroma.Style) string {
425431
// style's mode class alongside the existing class. Used for the outer
426432
// wrapper and standalone <body> so external CSS can target the mode.
427433
func (f *Formatter) styleAttrWithMode(styles map[chroma.TokenType]string, tt chroma.TokenType, style *chroma.Style) string {
428-
if !f.Classes {
434+
if !f.Classes || !f.modeClasses {
429435
return f.styleAttr(styles, tt)
430436
}
431437
cls := f.class(tt)
@@ -469,9 +475,15 @@ func (f *Formatter) writeCSSRule(w io.Writer, comment string, selector string, s
469475
// token's rule materialised explicitly for both themes.
470476
func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
471477
css := f.styleCache.get(style, false)
472-
modeCls := f.modeClass(style)
473-
chromaSel := fmt.Sprintf(".%schroma.%s", f.prefix, modeCls)
474-
bgSel := fmt.Sprintf(".%sbg.%s", f.prefix, modeCls)
478+
var chromaSel, bgSel string
479+
if f.modeClasses {
480+
modeCls := f.modeClass(style)
481+
chromaSel = fmt.Sprintf(".%schroma.%s", f.prefix, modeCls)
482+
bgSel = fmt.Sprintf(".%sbg.%s", f.prefix, modeCls)
483+
} else {
484+
chromaSel = fmt.Sprintf(".%schroma", f.prefix)
485+
bgSel = fmt.Sprintf(".%sbg", f.prefix)
486+
}
475487

476488
// Special-case background as it is mapped to the outer ".chroma" class.
477489
if err := f.writeCSSRule(w, chroma.Background.String(), bgSel, css[chroma.Background]); err != nil {

formatters/html/html_test.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func TestClassPrefix(t *testing.T) {
127127
var styleBuf bytes.Buffer
128128
err := withPrefix.WriteCSS(&styleBuf, styles.Fallback)
129129
assert.NoError(t, err)
130-
if !strings.Contains(styleBuf.String(), ".some-prefix-chroma.some-prefix-") {
130+
if !strings.Contains(styleBuf.String(), ".some-prefix-chroma ") {
131131
t.Error("Stylesheets should have a class prefix")
132132
}
133133
}
@@ -235,9 +235,9 @@ func TestPreWrapper(t *testing.T) {
235235
err = f.Format(&buf, styles.Fallback, it)
236236
assert.NoError(t, err)
237237

238-
assert.True(t, regexp.MustCompile("<body class=\"bg dark\">\n<pre.*class=\"chroma dark\"><code><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> FOO</span></span></code></pre>\n</body>\n</html>").MatchString(buf.String()))
239-
assert.True(t, regexp.MustCompile(`\.bg\.dark { .+ }`).MatchString(buf.String()))
240-
assert.True(t, regexp.MustCompile(`\.chroma\.dark { .+ }`).MatchString(buf.String()))
238+
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()))
239+
assert.True(t, regexp.MustCompile(`\.bg { .+ }`).MatchString(buf.String()))
240+
assert.True(t, regexp.MustCompile(`\.chroma { .+ }`).MatchString(buf.String()))
241241
}
242242

243243
func TestLinkeableLineNumbers(t *testing.T) {
@@ -264,7 +264,7 @@ func TestTableLinkeableLineNumbers(t *testing.T) {
264264

265265
assert.Contains(t, buf.String(), `id="line1"><a class="lnlinks" href="#line1">1</a>`)
266266
assert.Contains(t, buf.String(), `id="line5"><a class="lnlinks" href="#line5">5</a>`)
267-
assert.Contains(t, buf.String(), `/* LineLink */ .chroma.dark .lnlinks { outline: none; text-decoration: none; color: inherit }`, buf.String())
267+
assert.Contains(t, buf.String(), `/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }`, buf.String())
268268
}
269269

270270
func TestTableLineNumberSpacing(t *testing.T) {
@@ -335,7 +335,7 @@ func TestWithPreWrapper(t *testing.T) {
335335

336336
t.Run("Regular", func(t *testing.T) {
337337
s := format(New(WithClasses(true)))
338-
assert.Equal(t, s, `<pre class="chroma dark"><code><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></code></pre>`)
338+
assert.Equal(t, s, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></code></pre>`)
339339
})
340340

341341
t.Run("PreventSurroundingPre", func(t *testing.T) {
@@ -345,7 +345,7 @@ func TestWithPreWrapper(t *testing.T) {
345345

346346
t.Run("InlineCode", func(t *testing.T) {
347347
s := format(New(InlineCode(true), WithClasses(true)))
348-
assert.Equal(t, s, `<code class="chroma dark"><span class="nb">echo</span> FOO</code>`)
348+
assert.Equal(t, s, `<code class="chroma"><span class="nb">echo</span> FOO</code>`)
349349
})
350350

351351
t.Run("InlineCode, inline styles", func(t *testing.T) {
@@ -355,18 +355,18 @@ func TestWithPreWrapper(t *testing.T) {
355355

356356
t.Run("Wrapper", func(t *testing.T) {
357357
s := format(New(WithPreWrapper(wrapper), WithClasses(true)))
358-
assert.Equal(t, s, `<foo class="chroma dark" id="code-true"><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></foo>`)
358+
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>`)
359359
})
360360

361361
t.Run("Wrapper, LineNumbersInTable", func(t *testing.T) {
362362
s := format(New(WithPreWrapper(wrapper), WithClasses(true), WithLineNumbers(true), LineNumbersInTable(true)))
363363

364-
assert.Equal(t, s, `<div class="chroma dark">
364+
assert.Equal(t, s, `<div class="chroma">
365365
<table class="lntable"><tr><td class="lntd">
366-
<foo class="chroma dark" id="code-false"><span class="lnt">1
366+
<foo class="chroma" id="code-false"><span class="lnt">1
367367
</span></foo></td>
368368
<td class="lntd">
369-
<foo class="chroma dark" id="code-true"><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></foo></td></tr></table>
369+
<foo class="chroma" id="code-true"><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></foo></td></tr></table>
370370
</div>
371371
`)
372372
})
@@ -389,7 +389,7 @@ func TestReconfigureOptions(t *testing.T) {
389389
err = f.Format(&buf, styles.Fallback, it)
390390

391391
assert.NoError(t, err)
392-
assert.Equal(t, `<pre class="chroma dark"><code><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></code></pre>`, buf.String())
392+
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())
393393
}
394394

395395
func TestWriteCssWithAllClasses(t *testing.T) {
@@ -413,7 +413,7 @@ func TestModeClassOnWrapper(t *testing.T) {
413413
}
414414
for _, tt := range tests {
415415
t.Run(tt.name, func(t *testing.T) {
416-
f := New(WithClasses(true))
416+
f := New(WithClasses(true), WithModeClasses(true))
417417
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
418418
assert.NoError(t, err)
419419
var buf bytes.Buffer
@@ -425,7 +425,7 @@ func TestModeClassOnWrapper(t *testing.T) {
425425
}
426426

427427
func TestModeClassWithPrefix(t *testing.T) {
428-
f := New(WithClasses(true), ClassPrefix("c-"))
428+
f := New(WithClasses(true), WithModeClasses(true), ClassPrefix("c-"))
429429
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
430430
assert.NoError(t, err)
431431
var buf bytes.Buffer
@@ -456,7 +456,7 @@ func TestWriteCSSModeScoping(t *testing.T) {
456456
}
457457
for _, tt := range tests {
458458
t.Run(tt.name, func(t *testing.T) {
459-
f := New(WithClasses(true))
459+
f := New(WithClasses(true), WithModeClasses(true))
460460
var buf bytes.Buffer
461461
err := f.WriteCSS(&buf, styles.Get(tt.styleName))
462462
assert.NoError(t, err)

quick/example_test.go

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,73 +19,73 @@ func main() { }
1919
// Output:
2020
// <html>
2121
// <style type="text/css">
22-
// /* Background */ .bg.dark { color: #f8f8f2; background-color: #272822; }
23-
// /* PreWrapper */ .chroma.dark { color: #f8f8f2; background-color: #272822; -webkit-text-size-adjust: none; }
24-
// /* Error */ .chroma.dark .err { color: #960050; background-color: #1e0010 }
25-
// /* LineLink */ .chroma.dark .lnlinks { outline: none; text-decoration: none; color: inherit }
26-
// /* LineTableTD */ .chroma.dark .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
27-
// /* LineTable */ .chroma.dark .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
28-
// /* LineHighlight */ .chroma.dark .hl { background-color: #3c3d38 }
29-
// /* LineNumbersTable */ .chroma.dark .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
30-
// /* LineNumbers */ .chroma.dark .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
31-
// /* Line */ .chroma.dark .line { display: flex; }
32-
// /* Keyword */ .chroma.dark .k { color: #66d9ef }
33-
// /* KeywordConstant */ .chroma.dark .kc { color: #66d9ef }
34-
// /* KeywordDeclaration */ .chroma.dark .kd { color: #66d9ef }
35-
// /* KeywordNamespace */ .chroma.dark .kn { color: #f92672 }
36-
// /* KeywordPseudo */ .chroma.dark .kp { color: #66d9ef }
37-
// /* KeywordReserved */ .chroma.dark .kr { color: #66d9ef }
38-
// /* KeywordType */ .chroma.dark .kt { color: #66d9ef }
39-
// /* NameAttribute */ .chroma.dark .na { color: #a6e22e }
40-
// /* NameClass */ .chroma.dark .nc { color: #a6e22e }
41-
// /* NameConstant */ .chroma.dark .no { color: #66d9ef }
42-
// /* NameDecorator */ .chroma.dark .nd { color: #a6e22e }
43-
// /* NameException */ .chroma.dark .ne { color: #a6e22e }
44-
// /* NameOther */ .chroma.dark .nx { color: #a6e22e }
45-
// /* NameTag */ .chroma.dark .nt { color: #f92672 }
46-
// /* NameFunction */ .chroma.dark .nf { color: #a6e22e }
47-
// /* NameFunctionMagic */ .chroma.dark .fm { color: #a6e22e }
48-
// /* Literal */ .chroma.dark .l { color: #ae81ff }
49-
// /* LiteralDate */ .chroma.dark .ld { color: #e6db74 }
50-
// /* LiteralString */ .chroma.dark .s { color: #e6db74 }
51-
// /* LiteralStringAffix */ .chroma.dark .sa { color: #e6db74 }
52-
// /* LiteralStringBacktick */ .chroma.dark .sb { color: #e6db74 }
53-
// /* LiteralStringChar */ .chroma.dark .sc { color: #e6db74 }
54-
// /* LiteralStringDelimiter */ .chroma.dark .dl { color: #e6db74 }
55-
// /* LiteralStringDoc */ .chroma.dark .sd { color: #e6db74 }
56-
// /* LiteralStringDouble */ .chroma.dark .s2 { color: #e6db74 }
57-
// /* LiteralStringEscape */ .chroma.dark .se { color: #ae81ff }
58-
// /* LiteralStringHeredoc */ .chroma.dark .sh { color: #e6db74 }
59-
// /* LiteralStringInterpol */ .chroma.dark .si { color: #e6db74 }
60-
// /* LiteralStringOther */ .chroma.dark .sx { color: #e6db74 }
61-
// /* LiteralStringRegex */ .chroma.dark .sr { color: #e6db74 }
62-
// /* LiteralStringSingle */ .chroma.dark .s1 { color: #e6db74 }
63-
// /* LiteralStringSymbol */ .chroma.dark .ss { color: #e6db74 }
64-
// /* LiteralNumber */ .chroma.dark .m { color: #ae81ff }
65-
// /* LiteralNumberBin */ .chroma.dark .mb { color: #ae81ff }
66-
// /* LiteralNumberFloat */ .chroma.dark .mf { color: #ae81ff }
67-
// /* LiteralNumberHex */ .chroma.dark .mh { color: #ae81ff }
68-
// /* LiteralNumberInteger */ .chroma.dark .mi { color: #ae81ff }
69-
// /* LiteralNumberIntegerLong */ .chroma.dark .il { color: #ae81ff }
70-
// /* LiteralNumberOct */ .chroma.dark .mo { color: #ae81ff }
71-
// /* Operator */ .chroma.dark .o { color: #f92672 }
72-
// /* OperatorWord */ .chroma.dark .ow { color: #f92672 }
73-
// /* OperatorReserved */ .chroma.dark .or { color: #f92672 }
74-
// /* Comment */ .chroma.dark .c { color: #75715e }
75-
// /* CommentHashbang */ .chroma.dark .ch { color: #75715e }
76-
// /* CommentMultiline */ .chroma.dark .cm { color: #75715e }
77-
// /* CommentSingle */ .chroma.dark .c1 { color: #75715e }
78-
// /* CommentSpecial */ .chroma.dark .cs { color: #75715e }
79-
// /* CommentPreproc */ .chroma.dark .cp { color: #75715e }
80-
// /* CommentPreprocFile */ .chroma.dark .cpf { color: #75715e }
81-
// /* GenericDeleted */ .chroma.dark .gd { color: #f92672 }
82-
// /* GenericEmph */ .chroma.dark .ge { font-style: italic }
83-
// /* GenericInserted */ .chroma.dark .gi { color: #a6e22e }
84-
// /* GenericStrong */ .chroma.dark .gs { font-weight: bold }
85-
// /* GenericSubheading */ .chroma.dark .gu { color: #75715e }
22+
// /* Background */ .bg { color: #f8f8f2; background-color: #272822; }
23+
// /* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; -webkit-text-size-adjust: none; }
24+
// /* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
25+
// /* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
26+
// /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
27+
// /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
28+
// /* LineHighlight */ .chroma .hl { background-color: #3c3d38 }
29+
// /* 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 }
30+
// /* 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 }
31+
// /* Line */ .chroma .line { display: flex; }
32+
// /* Keyword */ .chroma .k { color: #66d9ef }
33+
// /* KeywordConstant */ .chroma .kc { color: #66d9ef }
34+
// /* KeywordDeclaration */ .chroma .kd { color: #66d9ef }
35+
// /* KeywordNamespace */ .chroma .kn { color: #f92672 }
36+
// /* KeywordPseudo */ .chroma .kp { color: #66d9ef }
37+
// /* KeywordReserved */ .chroma .kr { color: #66d9ef }
38+
// /* KeywordType */ .chroma .kt { color: #66d9ef }
39+
// /* NameAttribute */ .chroma .na { color: #a6e22e }
40+
// /* NameClass */ .chroma .nc { color: #a6e22e }
41+
// /* NameConstant */ .chroma .no { color: #66d9ef }
42+
// /* NameDecorator */ .chroma .nd { color: #a6e22e }
43+
// /* NameException */ .chroma .ne { color: #a6e22e }
44+
// /* NameOther */ .chroma .nx { color: #a6e22e }
45+
// /* NameTag */ .chroma .nt { color: #f92672 }
46+
// /* NameFunction */ .chroma .nf { color: #a6e22e }
47+
// /* NameFunctionMagic */ .chroma .fm { color: #a6e22e }
48+
// /* Literal */ .chroma .l { color: #ae81ff }
49+
// /* LiteralDate */ .chroma .ld { color: #e6db74 }
50+
// /* LiteralString */ .chroma .s { color: #e6db74 }
51+
// /* LiteralStringAffix */ .chroma .sa { color: #e6db74 }
52+
// /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 }
53+
// /* LiteralStringChar */ .chroma .sc { color: #e6db74 }
54+
// /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 }
55+
// /* LiteralStringDoc */ .chroma .sd { color: #e6db74 }
56+
// /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 }
57+
// /* LiteralStringEscape */ .chroma .se { color: #ae81ff }
58+
// /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 }
59+
// /* LiteralStringInterpol */ .chroma .si { color: #e6db74 }
60+
// /* LiteralStringOther */ .chroma .sx { color: #e6db74 }
61+
// /* LiteralStringRegex */ .chroma .sr { color: #e6db74 }
62+
// /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 }
63+
// /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 }
64+
// /* LiteralNumber */ .chroma .m { color: #ae81ff }
65+
// /* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
66+
// /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
67+
// /* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
68+
// /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
69+
// /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
70+
// /* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
71+
// /* Operator */ .chroma .o { color: #f92672 }
72+
// /* OperatorWord */ .chroma .ow { color: #f92672 }
73+
// /* OperatorReserved */ .chroma .or { color: #f92672 }
74+
// /* Comment */ .chroma .c { color: #75715e }
75+
// /* CommentHashbang */ .chroma .ch { color: #75715e }
76+
// /* CommentMultiline */ .chroma .cm { color: #75715e }
77+
// /* CommentSingle */ .chroma .c1 { color: #75715e }
78+
// /* CommentSpecial */ .chroma .cs { color: #75715e }
79+
// /* CommentPreproc */ .chroma .cp { color: #75715e }
80+
// /* CommentPreprocFile */ .chroma .cpf { color: #75715e }
81+
// /* GenericDeleted */ .chroma .gd { color: #f92672 }
82+
// /* GenericEmph */ .chroma .ge { font-style: italic }
83+
// /* GenericInserted */ .chroma .gi { color: #a6e22e }
84+
// /* GenericStrong */ .chroma .gs { font-weight: bold }
85+
// /* GenericSubheading */ .chroma .gu { color: #75715e }
8686
// body { color:#f8f8f2;background-color:#272822;; }
87-
// </style><body class="bg dark">
88-
// <pre class="chroma dark"><code><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
87+
// </style><body class="bg">
88+
// <pre class="chroma"><code><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
8989
// </span></span></span><span class="line"><span class="cl"><span class="w">
9090
// </span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">}</span><span class="w">
9191
// </span></span></span></code></pre>

0 commit comments

Comments
 (0)