fix(markup): don't escape ambiguous characters in MathML (#39493)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Nico Schlömer
2026-09-30 18:37:30 +02:00
committed by GitHub
parent 590d2984d9
commit 9b2a3c267b
3 changed files with 65 additions and 2 deletions
+40 -1
View File
@@ -8,11 +8,13 @@ import (
"fmt"
"html"
"io"
"strings"
"unicode"
"unicode/utf8"
"gitea.dev/modules/setting"
"gitea.dev/modules/translation"
"gitea.dev/modules/util"
)
type htmlChunkReader struct {
@@ -30,6 +32,10 @@ type escapeStreamer struct {
ambiguousTables []*AmbiguousTable
allowed map[rune]bool
tagPartial []byte // partial tag content, used to detect if we are in some tags
inTagMath bool // MathML operators like U+2212 are intended and wrapping them breaks the math layout
out io.Writer
}
@@ -62,6 +68,7 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
for i, part := range parts {
if partInTag[i] {
lastIsTag = true
es.trackHtmlTag(part)
if _, err := out.Write(part); err != nil {
return nil, err
}
@@ -75,7 +82,11 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
return nil, err
}
}
if err = es.detectAndWriteRunes(part); err != nil {
if es.inTagMath {
if _, err := out.Write(part); err != nil {
return nil, err
}
} else if err = es.detectAndWriteRunes(part); err != nil {
return nil, err
}
}
@@ -83,6 +94,34 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
}
}
// trackHtmlTag receives tag parts, a tag might be split into multiple parts
func (e *escapeStreamer) trackHtmlTag(part []byte) {
const maxHeadLen = 100 // only read the first N bytes of the tag for detection purpose
if part[0] == '<' {
// start a new tag
e.tagPartial = e.tagPartial[:0]
}
if len(e.tagPartial) >= maxHeadLen {
return
}
e.tagPartial = append(e.tagPartial, part[:min(len(part), maxHeadLen-len(e.tagPartial))]...)
isTag := func(prefix string) bool {
if len(e.tagPartial) < len(prefix)+1 {
return false
}
if !util.AsciiEqualFold(e.tagPartial[:len(prefix)], []byte(prefix)) {
return false
}
return strings.IndexByte(" \t\n\r\f>", e.tagPartial[len(prefix)]) != -1
}
if isTag("<math") {
e.inTagMath = true
} else if isTag("</math") {
e.inTagMath = false
}
}
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
if ok {