mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-01 21:44:57 +00:00
fix(markup): don't escape ambiguous characters in MathML (#39493)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -141,6 +141,12 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
|
||||
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
|
||||
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
||||
},
|
||||
{
|
||||
name: "ambiguous in math",
|
||||
text: "<math><mo>−</mo><mi>b</mi></math> −",
|
||||
result: `<math><mo>−</mo><mi>b</mi></math> <span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:− [U+2212],- [U+002D]"><span class="char">−</span></span>`,
|
||||
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
||||
},
|
||||
}
|
||||
|
||||
func TestEscapeControlReader(t *testing.T) {
|
||||
@@ -156,6 +162,24 @@ func TestEscapeControlReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackHtmlTag(t *testing.T) {
|
||||
e := &escapeStreamer{}
|
||||
for _, tt := range []struct {
|
||||
parts []string
|
||||
inMath bool
|
||||
}{
|
||||
{[]string{"<ma", `TH display="block">`}, true},
|
||||
{[]string{"<mo>"}, true},
|
||||
{[]string{"</MA", "th>"}, false},
|
||||
{[]string{"<mathx>"}, false},
|
||||
} {
|
||||
for _, part := range tt.parts {
|
||||
e.trackHtmlTag([]byte(part))
|
||||
}
|
||||
assert.Equal(t, tt.inMath, e.inTagMath, "%v", tt.parts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
||||
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
|
||||
|
||||
@@ -121,7 +121,7 @@ func asciiLower(b byte) byte {
|
||||
|
||||
// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
|
||||
// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
|
||||
func AsciiEqualFold(s, t string) bool {
|
||||
func AsciiEqualFold[T string | []byte](s, t T) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user