refactor: render highlight language (#38793)

Avoid CSS injection

More details are in the comment of CodeBlockAttributes
This commit is contained in:
wxiaoguang
2026-08-06 18:07:36 +08:00
committed by GitHub
parent 231ba1da19
commit 6ff3a65708
17 changed files with 112 additions and 56 deletions
+50
View File
@@ -8,8 +8,10 @@ import (
"bytes"
gohtml "html"
"html/template"
"strings"
"sync"
"gitea.dev/modules/htmlutil"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
@@ -161,3 +163,51 @@ func formatLexerName(name string) string {
}
return util.ToTitleCaseNoLower(name)
}
func languageForCssAttrName(lang string) (forCSS, forAttr string) {
s := strings.ToLower(lang)
if s == "" || s == LanguagePlaintext || s == chromaLexerFallback {
return "text", "text"
}
isValid := func(c byte) bool {
// although "-" is valid in CSS name, it is used as a field separator, so we don't want to keep it in the name
return 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_'
}
idx := 0
for ; idx < len(s); idx++ {
if !isValid(s[idx]) {
break
}
}
if idx == len(s) {
return s, lang
}
out := []byte(s)
for i := idx; i < len(s); i++ {
if !isValid(out[i]) {
out[i] = '_'
}
}
return string(out), lang
}
func CodeBlockAttributes(lang string) (preAttrs, codeAttrs template.HTML) {
// Code block's "chroma" class is used to highlight the code.
// "language-{LanguageName}" class is used as part of commonmark spec.
// It's unclear about how to handle special chars for a language name like "Visual Basic.NET" or "C++" or "F#".
// The commonmark spec seems wrong: https://spec.commonmark.org/0.31.2/#info-string, it just outputs invalid CSS class names.
cssName, attrLang := languageForCssAttrName(lang)
renderByFrontend := lang == "mermaid" || lang == "math"
preExtraClasses := ""
if renderByFrontend {
preExtraClasses = " is-loading"
}
// The "math.ts" strictly depends on the structure: <pre class="code-block"><code class="language-math">...</code></pre>
// * If "pre" exists, it is rendered as "block", otherwise, it is rendered as "inline"
// The "mermaid.ts" also strictly depends on the structure: "pre" must exist because it is always rendered as "block".
//
// Hint: "data-code-language" is not exposed in some cases due to the Markup sanitizer, the rules can be refactored in the future if the attribute is useful.
return htmlutil.HTMLFormat(`class="code-block%s"`, preExtraClasses), htmlutil.HTMLFormat(`class="chroma language-%s" data-code-language="%s"`, cssName, attrLang)
}
+16
View File
@@ -216,3 +216,19 @@ func TestUnsafeSplitHighlightedLines(t *testing.T) {
assert.Equal(t, "<span>a</span>\n", string(ret[0]))
assert.Equal(t, "<span>b\n</span>", string(ret[1]))
}
func TestCodeBlockAttributes(t *testing.T) {
test := func(t *testing.T, lang string, css, attr template.HTML) {
t.Helper()
cssActual, attrActual := CodeBlockAttributes(lang)
assert.Equal(t, css, cssActual)
assert.Equal(t, attr, attrActual)
}
for _, s := range []string{"", "FALLback", "plainTEXT"} {
test(t, s, `class="code-block"`, `class="chroma language-text" data-code-language="text"`)
}
test(t, "math", `class="code-block is-loading"`, `class="chroma language-math" data-code-language="math"`)
test(t, "mermaid", `class="code-block is-loading"`, `class="chroma language-mermaid" data-code-language="mermaid"`)
test(t, "Visual Basic.NET", `class="code-block"`, `class="chroma language-visual_basic_net" data-code-language="Visual Basic.NET"`)
test(t, "c++-x", `class="code-block"`, `class="chroma language-c___x" data-code-language="c++-x"`)
}