filtered := make([]rune, 0, len(InvisibleRunes))
@@ -47,8 +96,8 @@ Usage: %[1]s [-v] [-o output.go]
}
table := rangetable.New(filtered...)
- if err := runTemplate(generatorTemplate, output, table); err != nil {
- fatalf("Unable to run template: %v", err)
+ if err := runTemplate(generatorInvisible, "../invisible_gen.go", table); err != nil {
+ log.Fatalf("Unable to run template: %v", err)
}
}
@@ -59,7 +108,7 @@ func runTemplate(t *template.Template, filename string, data any) error {
}
bs, err := format.Source(buf.Bytes())
if err != nil {
- verbosef("Bad source:\n%s", buf.String())
+ log.Printf("Bad source:\n%s", buf.String())
return fmt.Errorf("unable to format source: %w", err)
}
@@ -85,37 +134,68 @@ func runTemplate(t *template.Template, filename string, data any) error {
return nil
}
-var generatorTemplate = template.Must(template.New("invisibleTemplate").Parse(`// This file is generated by modules/charset/invisible/generate.go DO NOT EDIT
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
+func main() {
+ generateAmbiguous()
+ generateInvisible()
+}
+var templateAmbiguous = template.Must(template.New("ambiguousTemplate").Parse(`// This file is generated by modules/charset/generate/generate.go DO NOT EDIT
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
package charset
import "unicode"
-var InvisibleRanges = &unicode.RangeTable{
- R16: []unicode.Range16{
-{{range .R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}},
-{{end}} },
- R32: []unicode.Range32{
-{{range .R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}},
-{{end}} },
- LatinOffset: {{.LatinOffset}},
-}
-`))
+// This file is generated from https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json
-func logf(format string, args ...any) {
- fmt.Fprintf(os.Stderr, format+"\n", args...)
+// AmbiguousTable matches a confusable rune with its partner for the Locale
+type AmbiguousTable struct {
+ Confusable []rune
+ With []rune
+ Locale string
+ RangeTable *unicode.RangeTable
}
-func verbosef(format string, args ...any) {
- if verbose {
- logf(format, args...)
+func newAmbiguousTableMap() map[string]*AmbiguousTable {
+ return map[string]*AmbiguousTable {
+ {{- range .Tables}}
+ {{printf "%q" .Locale}}: {
+ Confusable: []rune{ {{range .Confusable}}{{.}},{{end}} },
+ With: []rune{ {{range .With}}{{.}},{{end}} },
+ Locale: {{printf "%q" .Locale}},
+ RangeTable: &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {{range .RangeTable.R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}},
+ {{end}} },
+ R32: []unicode.Range32{
+ {{range .RangeTable.R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}},
+ {{end}} },
+ LatinOffset: {{.RangeTable.LatinOffset}},
+ },
+ },
+ {{end}}
}
}
+`))
-func fatalf(format string, args ...any) {
- logf("fatal: "+format+"\n", args...)
- os.Exit(1)
+var generatorInvisible = template.Must(template.New("invisibleTemplate").Parse(`// This file is generated by modules/charset/generate/generate.go DO NOT EDIT
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package charset
+
+import "unicode"
+
+func newInvisibleRangeTable() *unicode.RangeTable {
+ return &unicode.RangeTable{
+ R16: []unicode.Range16{
+{{range .R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}},
+ {{end}}},
+ R32: []unicode.Range32{
+{{range .R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}},
+ {{end}}},
+ LatinOffset: {{.LatinOffset}},
+ }
}
+`))
diff --git a/modules/charset/htmlstream.go b/modules/charset/htmlstream.go
deleted file mode 100644
index 61f29120a6541..0000000000000
--- a/modules/charset/htmlstream.go
+++ /dev/null
@@ -1,200 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package charset
-
-import (
- "fmt"
- "io"
-
- "golang.org/x/net/html"
-)
-
-// HTMLStreamer represents a SAX-like interface for HTML
-type HTMLStreamer interface {
- Error(err error) error
- Doctype(data string) error
- Comment(data string) error
- StartTag(data string, attrs ...html.Attribute) error
- SelfClosingTag(data string, attrs ...html.Attribute) error
- EndTag(data string) error
- Text(data string) error
-}
-
-// PassthroughHTMLStreamer is a passthrough streamer
-type PassthroughHTMLStreamer struct {
- next HTMLStreamer
-}
-
-func NewPassthroughStreamer(next HTMLStreamer) *PassthroughHTMLStreamer {
- return &PassthroughHTMLStreamer{next: next}
-}
-
-var _ (HTMLStreamer) = &PassthroughHTMLStreamer{}
-
-// Error tells the next streamer in line that there is an error
-func (p *PassthroughHTMLStreamer) Error(err error) error {
- return p.next.Error(err)
-}
-
-// Doctype tells the next streamer what the doctype is
-func (p *PassthroughHTMLStreamer) Doctype(data string) error {
- return p.next.Doctype(data)
-}
-
-// Comment tells the next streamer there is a comment
-func (p *PassthroughHTMLStreamer) Comment(data string) error {
- return p.next.Comment(data)
-}
-
-// StartTag tells the next streamer there is a starting tag
-func (p *PassthroughHTMLStreamer) StartTag(data string, attrs ...html.Attribute) error {
- return p.next.StartTag(data, attrs...)
-}
-
-// SelfClosingTag tells the next streamer there is a self-closing tag
-func (p *PassthroughHTMLStreamer) SelfClosingTag(data string, attrs ...html.Attribute) error {
- return p.next.SelfClosingTag(data, attrs...)
-}
-
-// EndTag tells the next streamer there is a end tag
-func (p *PassthroughHTMLStreamer) EndTag(data string) error {
- return p.next.EndTag(data)
-}
-
-// Text tells the next streamer there is a text
-func (p *PassthroughHTMLStreamer) Text(data string) error {
- return p.next.Text(data)
-}
-
-// HTMLStreamWriter acts as a writing sink
-type HTMLStreamerWriter struct {
- io.Writer
- err error
-}
-
-// Write implements io.Writer
-func (h *HTMLStreamerWriter) Write(data []byte) (int, error) {
- if h.err != nil {
- return 0, h.err
- }
- return h.Writer.Write(data)
-}
-
-// Write implements io.StringWriter
-func (h *HTMLStreamerWriter) WriteString(data string) (int, error) {
- if h.err != nil {
- return 0, h.err
- }
- return h.Writer.Write([]byte(data))
-}
-
-// Error tells the next streamer in line that there is an error
-func (h *HTMLStreamerWriter) Error(err error) error {
- if h.err == nil {
- h.err = err
- }
- return h.err
-}
-
-// Doctype tells the next streamer what the doctype is
-func (h *HTMLStreamerWriter) Doctype(data string) error {
- _, h.err = h.WriteString("")
- return h.err
-}
-
-// Comment tells the next streamer there is a comment
-func (h *HTMLStreamerWriter) Comment(data string) error {
- _, h.err = h.WriteString("")
- return h.err
-}
-
-// StartTag tells the next streamer there is a starting tag
-func (h *HTMLStreamerWriter) StartTag(data string, attrs ...html.Attribute) error {
- return h.startTag(data, attrs, false)
-}
-
-// SelfClosingTag tells the next streamer there is a self-closing tag
-func (h *HTMLStreamerWriter) SelfClosingTag(data string, attrs ...html.Attribute) error {
- return h.startTag(data, attrs, true)
-}
-
-func (h *HTMLStreamerWriter) startTag(data string, attrs []html.Attribute, selfclosing bool) error {
- if _, h.err = h.WriteString("<" + data); h.err != nil {
- return h.err
- }
- for _, attr := range attrs {
- if _, h.err = h.WriteString(" " + attr.Key + "=\"" + html.EscapeString(attr.Val) + "\""); h.err != nil {
- return h.err
- }
- }
- if selfclosing {
- if _, h.err = h.WriteString("/>"); h.err != nil {
- return h.err
- }
- } else {
- if _, h.err = h.WriteString(">"); h.err != nil {
- return h.err
- }
- }
- return h.err
-}
-
-// EndTag tells the next streamer there is a end tag
-func (h *HTMLStreamerWriter) EndTag(data string) error {
- _, h.err = h.WriteString("" + data + ">")
- return h.err
-}
-
-// Text tells the next streamer there is a text
-func (h *HTMLStreamerWriter) Text(data string) error {
- _, h.err = h.WriteString(html.EscapeString(data))
- return h.err
-}
-
-// StreamHTML streams an html to a provided streamer
-func StreamHTML(source io.Reader, streamer HTMLStreamer) error {
- tokenizer := html.NewTokenizer(source)
- for {
- tt := tokenizer.Next()
- switch tt {
- case html.ErrorToken:
- if tokenizer.Err() != io.EOF {
- return tokenizer.Err()
- }
- return nil
- case html.DoctypeToken:
- token := tokenizer.Token()
- if err := streamer.Doctype(token.Data); err != nil {
- return err
- }
- case html.CommentToken:
- token := tokenizer.Token()
- if err := streamer.Comment(token.Data); err != nil {
- return err
- }
- case html.StartTagToken:
- token := tokenizer.Token()
- if err := streamer.StartTag(token.Data, token.Attr...); err != nil {
- return err
- }
- case html.SelfClosingTagToken:
- token := tokenizer.Token()
- if err := streamer.StartTag(token.Data, token.Attr...); err != nil {
- return err
- }
- case html.EndTagToken:
- token := tokenizer.Token()
- if err := streamer.EndTag(token.Data); err != nil {
- return err
- }
- case html.TextToken:
- token := tokenizer.Token()
- if err := streamer.Text(token.Data); err != nil {
- return err
- }
- default:
- return fmt.Errorf("unknown type of token: %d", tt)
- }
- }
-}
diff --git a/modules/charset/invisible_gen.go b/modules/charset/invisible_gen.go
index 812f0e34b3a73..ddda875a9fd76 100644
--- a/modules/charset/invisible_gen.go
+++ b/modules/charset/invisible_gen.go
@@ -1,36 +1,38 @@
-// This file is generated by modules/charset/invisible/generate.go DO NOT EDIT
-// Copyright 2022 The Gitea Authors. All rights reserved.
+// This file is generated by modules/charset/generate/generate.go DO NOT EDIT
+// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package charset
import "unicode"
-var InvisibleRanges = &unicode.RangeTable{
- R16: []unicode.Range16{
- {Lo: 11, Hi: 13, Stride: 1},
- {Lo: 127, Hi: 160, Stride: 33},
- {Lo: 173, Hi: 847, Stride: 674},
- {Lo: 1564, Hi: 4447, Stride: 2883},
- {Lo: 4448, Hi: 6068, Stride: 1620},
- {Lo: 6069, Hi: 6155, Stride: 86},
- {Lo: 6156, Hi: 6158, Stride: 1},
- {Lo: 7355, Hi: 7356, Stride: 1},
- {Lo: 8192, Hi: 8207, Stride: 1},
- {Lo: 8234, Hi: 8239, Stride: 1},
- {Lo: 8287, Hi: 8303, Stride: 1},
- {Lo: 10240, Hi: 12288, Stride: 2048},
- {Lo: 12644, Hi: 65024, Stride: 52380},
- {Lo: 65025, Hi: 65039, Stride: 1},
- {Lo: 65279, Hi: 65440, Stride: 161},
- {Lo: 65520, Hi: 65528, Stride: 1},
- {Lo: 65532, Hi: 65532, Stride: 1},
- },
- R32: []unicode.Range32{
- {Lo: 78844, Hi: 119155, Stride: 40311},
- {Lo: 119156, Hi: 119162, Stride: 1},
- {Lo: 917504, Hi: 917631, Stride: 1},
- {Lo: 917760, Hi: 917999, Stride: 1},
- },
- LatinOffset: 2,
+func newInvisibleRangeTable() *unicode.RangeTable {
+ return &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {Lo: 11, Hi: 13, Stride: 1},
+ {Lo: 127, Hi: 160, Stride: 33},
+ {Lo: 173, Hi: 847, Stride: 674},
+ {Lo: 1564, Hi: 4447, Stride: 2883},
+ {Lo: 4448, Hi: 6068, Stride: 1620},
+ {Lo: 6069, Hi: 6155, Stride: 86},
+ {Lo: 6156, Hi: 6158, Stride: 1},
+ {Lo: 7355, Hi: 7356, Stride: 1},
+ {Lo: 8192, Hi: 8207, Stride: 1},
+ {Lo: 8234, Hi: 8239, Stride: 1},
+ {Lo: 8287, Hi: 8303, Stride: 1},
+ {Lo: 10240, Hi: 12288, Stride: 2048},
+ {Lo: 12644, Hi: 65024, Stride: 52380},
+ {Lo: 65025, Hi: 65039, Stride: 1},
+ {Lo: 65279, Hi: 65440, Stride: 161},
+ {Lo: 65520, Hi: 65528, Stride: 1},
+ {Lo: 65532, Hi: 65532, Stride: 1},
+ },
+ R32: []unicode.Range32{
+ {Lo: 78844, Hi: 119155, Stride: 40311},
+ {Lo: 119156, Hi: 119162, Stride: 1},
+ {Lo: 917504, Hi: 917631, Stride: 1},
+ {Lo: 917760, Hi: 917999, Stride: 1},
+ },
+ LatinOffset: 2,
+ }
}
diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go
index addc372f85f8b..dca28588e4702 100644
--- a/modules/highlight/highlight.go
+++ b/modules/highlight/highlight.go
@@ -6,8 +6,8 @@ package highlight
import (
"bytes"
+ gohtml "html"
"html/template"
- "slices"
"sync"
"code.gitea.io/gitea/modules/log"
@@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/alecthomas/chroma/v2"
- "github.com/alecthomas/chroma/v2/formatters/html"
+ chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
)
@@ -25,8 +25,6 @@ const sizeLimit = 1024 * 1024
type globalVarsType struct {
highlightMapping map[string]string
githubStyles *chroma.Style
- escapeFull []template.HTML
- escCtrlCharsMap []template.HTML
}
var (
@@ -42,69 +40,10 @@ func globalVars() *globalVarsType {
globalVarsPtr = &globalVarsType{}
globalVarsPtr.githubStyles = styles.Get("github")
globalVarsPtr.highlightMapping = setting.GetHighlightMapping()
- globalVarsPtr.escCtrlCharsMap = make([]template.HTML, 256)
- // ASCII Table 0x00 - 0x1F
- controlCharNames := []string{
- "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL",
- "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI",
- "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB",
- "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US",
- }
- // Uncomment this line if you'd debug the layout without creating a special file, then Space (0x20) will also be escaped.
- // Don't worry, even if you forget to comment it out and push it to git repo, the CI tests will catch it and fail.
- // controlCharNames = append(controlCharNames, "SP")
- for i, s := range controlCharNames {
- globalVarsPtr.escCtrlCharsMap[i] = template.HTML(`` + string(byte(i)) + ` `)
- }
- globalVarsPtr.escCtrlCharsMap[0x7f] = template.HTML(`` + string(byte(0x7f)) + ` `)
- globalVarsPtr.escCtrlCharsMap['\t'] = ""
- globalVarsPtr.escCtrlCharsMap['\n'] = ""
- globalVarsPtr.escCtrlCharsMap['\r'] = ""
-
- globalVarsPtr.escapeFull = slices.Clone(globalVarsPtr.escCtrlCharsMap)
- // exactly the same as Golang's html.EscapeString
- globalVarsPtr.escapeFull['&'] = "&"
- globalVarsPtr.escapeFull['\''] = "'"
- globalVarsPtr.escapeFull['<'] = "<"
- globalVarsPtr.escapeFull['>'] = ">"
- globalVarsPtr.escapeFull['"'] = """
}
return globalVarsPtr
}
-func escapeByMap(code []byte, escapeMap []template.HTML) template.HTML {
- firstEscapePos := -1
- for i, c := range code {
- if escapeMap[c] != "" {
- firstEscapePos = i
- break
- }
- }
- if firstEscapePos == -1 {
- return template.HTML(util.UnsafeBytesToString(code))
- }
-
- buf := make([]byte, firstEscapePos, len(code)*2)
- copy(buf[:firstEscapePos], code[:firstEscapePos])
- for i := firstEscapePos; i < len(code); i++ {
- c := code[i]
- if esc := escapeMap[c]; esc != "" {
- buf = append(buf, esc...)
- } else {
- buf = append(buf, c)
- }
- }
- return template.HTML(util.UnsafeBytesToString(buf))
-}
-
-func escapeFullString(code string) template.HTML {
- return escapeByMap(util.UnsafeStringToBytes(code), globalVars().escapeFull)
-}
-
-func escapeControlChars(code []byte) template.HTML {
- return escapeByMap(code, globalVars().escCtrlCharsMap)
-}
-
// UnsafeSplitHighlightedLines splits highlighted code into lines preserving HTML tags
// It always includes '\n', '\n' can appear at the end of each line or in the middle of HTML tags
// The '\n' is necessary for copying code from web UI to preserve original code lines
@@ -137,6 +76,10 @@ func UnsafeSplitHighlightedLines(code template.HTML) (ret [][]byte) {
}
}
+func htmlEscape(code string) template.HTML {
+ return template.HTML(gohtml.EscapeString(code))
+}
+
// RenderCodeSlowGuess tries to get a lexer by file name and language first,
// if not found, it will try to guess the lexer by code content, which is slow (more than several hundreds of milliseconds).
func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, lexer chroma.Lexer, lexerDisplayName string) {
@@ -147,7 +90,7 @@ func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML,
}
if len(code) > sizeLimit {
- return escapeFullString(code), nil, ""
+ return htmlEscape(code), nil, ""
}
lexer = detectChromaLexerWithAnalyze(fileName, language, util.UnsafeStringToBytes(code)) // it is also slow
@@ -156,15 +99,15 @@ func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML,
// RenderCodeByLexer returns a HTML version of code string with chroma syntax highlighting classes
func RenderCodeByLexer(lexer chroma.Lexer, code string) template.HTML {
- formatter := html.New(html.WithClasses(true),
- html.WithLineNumbers(false),
- html.PreventSurroundingPre(true),
+ formatter := chromahtml.New(chromahtml.WithClasses(true),
+ chromahtml.WithLineNumbers(false),
+ chromahtml.PreventSurroundingPre(true),
)
iterator, err := lexer.Tokenise(nil, code)
if err != nil {
log.Error("Can't tokenize code: %v", err)
- return escapeFullString(code)
+ return htmlEscape(code)
}
htmlBuf := &bytes.Buffer{}
@@ -172,14 +115,9 @@ func RenderCodeByLexer(lexer chroma.Lexer, code string) template.HTML {
err = formatter.Format(htmlBuf, globalVars().githubStyles, iterator)
if err != nil {
log.Error("Can't format code: %v", err)
- return escapeFullString(code)
+ return htmlEscape(code)
}
-
- // At the moment, we do not escape control chars here (unlike RenderFullFile which escapes control chars).
- // The reason is: it is a very rare case that a text file contains control chars.
- // This function is usually used by highlight diff and blame, not quite sure whether there will be side effects.
- // If there would be new user feedback about this, we can re-consider about various edge cases.
- return template.HTML(htmlBuf.String())
+ return template.HTML(util.UnsafeBytesToString(htmlBuf.Bytes()))
}
// RenderFullFile returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
@@ -191,10 +129,9 @@ func RenderFullFile(fileName, language string, code []byte) ([]template.HTML, st
lexerName := formatLexerName(lexer.Config().Name)
rendered := RenderCodeByLexer(lexer, util.UnsafeBytesToString(code))
unsafeLines := UnsafeSplitHighlightedLines(rendered)
- lines := make([]template.HTML, 0, len(unsafeLines))
- for _, lineBytes := range unsafeLines {
- line := escapeControlChars(lineBytes)
- lines = append(lines, line)
+ lines := make([]template.HTML, len(unsafeLines))
+ for idx, lineBytes := range unsafeLines {
+ lines[idx] = template.HTML(util.UnsafeBytesToString(lineBytes))
}
return lines, lexerName
}
@@ -213,7 +150,7 @@ func renderPlainText(code []byte) []template.HTML {
content = code[pos : pos+nextPos+1]
pos += nextPos + 1
}
- lines = append(lines, escapeFullString(util.UnsafeBytesToString(content)))
+ lines = append(lines, htmlEscape(util.UnsafeBytesToString(content)))
}
return lines
}
diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go
index cad22ba9bb7ee..211132b255f60 100644
--- a/modules/highlight/highlight_test.go
+++ b/modules/highlight/highlight_test.go
@@ -204,14 +204,3 @@ func TestUnsafeSplitHighlightedLines(t *testing.T) {
assert.Equal(t, "a \n", string(ret[0]))
assert.Equal(t, "b\n ", string(ret[1]))
}
-
-func TestEscape(t *testing.T) {
- assert.Equal(t, template.HTML("\t\r\n\x00 \x1f &'\"<>"), escapeControlChars([]byte("\t\r\n\x00\x1f&'\"<>")))
- assert.Equal(t, template.HTML("\x00 \x1f &'"<>\t\r\n"), escapeFullString("\x00\x1f&'\"<>\t\r\n"))
-
- out, _ := RenderFullFile("a.py", "", []byte("# \x7f<>"))
- assert.Equal(t, template.HTML(`# `+string(byte(0x7f))+` <> `), out[0])
-
- out = renderPlainText([]byte("# \x7f<>"))
- assert.Equal(t, template.HTML(`# `+string(byte(0x7f))+` <>`), out[0])
-}
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 009d659d761bc..b5eb5116b061b 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -74,7 +74,7 @@ func HighlightSearchResultCode(filename, language string, lineNums []int, code s
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
lexer := highlight.DetectChromaLexerByFileName(filename, language)
hl := highlight.RenderCodeByLexer(lexer, code)
- highlightedLines := strings.Split(string(hl), "\n")
+ highlightedLines := highlight.UnsafeSplitHighlightedLines(hl)
// The lineNums outputted by render might not match the original lineNums, because "highlight" removes the last `\n`
lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums)))
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 7ff0b204f0440..081a13fb9ec1c 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -16,6 +16,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
@@ -277,3 +278,24 @@ func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize in
extraIcon := svg.RenderHTML(info.GetExtraIconName(), iconSize)
return htmlutil.HTMLFormat(``, info.GetDescription(), icon, info.DisplayName, extraIcon)
}
+
+func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML {
+ if escapeStatus == nil || !escapeStatus.Escaped {
+ return ""
+ }
+ locale := ut.ctx.Value(translation.ContextKey).(translation.Locale)
+ var title template.HTML
+ if escapeStatus.HasAmbiguous {
+ title += locale.Tr("repo.ambiguous_runes_line")
+ } else if escapeStatus.HasInvisible {
+ title += locale.Tr("repo.invisible_runes_line")
+ }
+ return htmlutil.HTMLFormat(` `, title)
+}
+
+func (ut *RenderUtils) RenderUnicodeEscapeToggleTd(combined, escapeStatus *charset.EscapeStatus) template.HTML {
+ if combined == nil || !combined.Escaped {
+ return ""
+ }
+ return `` + ut.RenderUnicodeEscapeToggleButton(escapeStatus) + ` `
+}
diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json
index acb5a25087c57..a361781768752 100644
--- a/options/locale/locale_en-US.json
+++ b/options/locale/locale_en-US.json
@@ -1216,7 +1216,7 @@
"repo.ambiguous_runes_description": "This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.",
"repo.invisible_runes_line": "This line has invisible unicode characters",
"repo.ambiguous_runes_line": "This line has ambiguous unicode characters",
- "repo.ambiguous_character": "%[1]c [U+%04[1]X] can be confused with %[2]c [U+%04[2]X]",
+ "repo.ambiguous_character": "%[1]s can be confused with %[2]s",
"repo.escape_control_characters": "Escape",
"repo.unescape_control_characters": "Unescape",
"repo.file_copy_permalink": "Copy Permalink",
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 8283d3ad9dfe8..5cfe08c7fe081 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -17,7 +17,9 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/badge"
+ "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
@@ -190,9 +192,33 @@ func prepareMockData(ctx *context.Context) {
prepareMockDataBadgeActionsSvg(ctx)
case "/devtest/relative-time":
prepareMockDataRelativeTime(ctx)
+ case "/devtest/unicode-escape":
+ prepareMockDataUnicodeEscape(ctx)
}
}
+func prepareMockDataUnicodeEscape(ctx *context.Context) {
+ content := "// demo code\n"
+ content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n"
+ content += "if O๐พ { } // ambiguous char\n"
+ content += "if O๐พ && accessLevel != \"user\u202E \u2066// ambiguous char + invisible char\u2069 \u2066\" { }\n"
+ content += "str := `\xef` // broken char\n"
+ content += "str := `\x00 \x19 \x7f` // control char\n"
+
+ lineNums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
+
+ highlightLines := code.HighlightSearchResultCode("demo.go", "", lineNums, content)
+ escapeStatus := &charset.EscapeStatus{}
+ lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
+ for i, hl := range highlightLines {
+ lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, ctx.Locale)
+ escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
+ }
+ ctx.Data["HighlightLines"] = highlightLines
+ ctx.Data["EscapeStatus"] = escapeStatus
+ ctx.Data["LineEscapeStatus"] = lineEscapeStatus
+}
+
func TmplCommon(ctx *context.Context) {
prepareMockData(ctx)
if ctx.Req.Method == http.MethodPost {
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 7136b87058f11..46661f0df0fe4 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -159,7 +159,7 @@ func markupRenderToHTML(ctx *context.Context, renderCtx *markup.RenderContext, r
go func() {
sb := &strings.Builder{}
if markup.RendererNeedPostProcess(renderer) {
- escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) // We allow NBSP here this is rendered
+ escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.EscapeOptionsForView())
} else {
escaped = &charset.EscapeStatus{}
_, _ = io.Copy(sb, markupRd)
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index e5b07633a2108..1826ca54e1e05 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -258,8 +258,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
defer markupWr.Close()
done := make(chan struct{})
go func() {
- // We allow NBSP here this is rendered
- escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP)
+ escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.EscapeOptionsForView())
output = template.HTML(buf.String())
buf.Reset()
close(done)
diff --git a/services/markup/renderhelper_codepreview.go b/services/markup/renderhelper_codepreview.go
index 87c3bd1d4828b..fd2db0d10f5c7 100644
--- a/services/markup/renderhelper_codepreview.go
+++ b/services/markup/renderhelper_codepreview.go
@@ -101,7 +101,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
escapeStatus := &charset.EscapeStatus{}
lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
for i, hl := range highlightLines {
- lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.RuneNBSP)
+ lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.EscapeOptionsForView())
escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
}
diff --git a/services/markup/renderhelper_codepreview_test.go b/services/markup/renderhelper_codepreview_test.go
index c84845e7ea2cc..94ed5b418aafe 100644
--- a/services/markup/renderhelper_codepreview_test.go
+++ b/services/markup/renderhelper_codepreview_test.go
@@ -37,10 +37,12 @@ func TestRenderHelperCodePreview(t *testing.T) {
- # repo1
+ # repo1
+
-
+
+
@@ -64,7 +66,8 @@ func TestRenderHelperCodePreview(t *testing.T) {
diff --git a/templates/base/markup_codepreview.tmpl b/templates/base/markup_codepreview.tmpl
index e16848581d949..4c04b5092c775 100644
--- a/templates/base/markup_codepreview.tmpl
+++ b/templates/base/markup_codepreview.tmpl
@@ -13,10 +13,7 @@
{{- range $idx, $line := .HighlightLines -}}
- {{- if $.EscapeStatus.Escaped -}}
- {{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}}
- {{if $lineEscapeStatus.Escaped}} {{end}}
- {{- end}}
+ {{- ctx.RenderUtils.RenderUnicodeEscapeToggleTd $.EscapeStatus (index $.LineEscapeStatus $idx)}}
{{$line.FormattedContent}}
{{/* only div works, span generates incorrect HTML structure */}}
{{- end -}}
diff --git a/templates/devtest/unicode-escape.tmpl b/templates/devtest/unicode-escape.tmpl
new file mode 100644
index 0000000000000..a61813f5c8eae
--- /dev/null
+++ b/templates/devtest/unicode-escape.tmpl
@@ -0,0 +1,17 @@
+{{template "devtest/devtest-header"}}
+
+
+
+
+ {{range $idx, $line := .HighlightLines}}
+
+
+ {{ctx.RenderUtils.RenderUnicodeEscapeToggleTd $.EscapeStatus (index $.LineEscapeStatus $idx)}}
+ {{$line.FormattedContent}}
+
+ {{end}}
+
+
+
+
+{{template "devtest/devtest-footer"}}
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index bc91adb64f776..51052d9359460 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -66,13 +66,9 @@
- {{if $.EscapeStatus.Escaped}}
-
- {{if $row.EscapeStatus.Escaped}}
-
- {{end}}
-
- {{end}}
+
+ {{ctx.RenderUtils.RenderUnicodeEscapeToggleTd $.EscapeStatus $row.EscapeStatus}}
+
{{$row.Code}}
diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl
index 916d589839ac5..a2dcf0d091078 100644
--- a/templates/repo/diff/blob_excerpt.tmpl
+++ b/templates/repo/diff/blob_excerpt.tmpl
@@ -12,7 +12,7 @@
{{else}}
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
- {{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}} {{end}}
+ {{if $line.LeftIdx}}{{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $inlineDiff.EscapeStatus}}{{end}}
{{if $line.LeftIdx}} {{end}}
{{/* ATTENTION: BLOB-EXCERPT-COMMENT-RIGHT: here it intentionally use "right" side to comment, because the backend code depends on the assumption that the comment only happens on right side*/}}
@@ -28,7 +28,7 @@
{{- end -}}
- {{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}} {{end}}
+ {{if and $line.RightIdx}}{{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $inlineDiff.EscapeStatus}}{{end}}
{{if $line.RightIdx}} {{end}}
{{- if and $canCreateComment $line.RightIdx -}}
@@ -69,7 +69,7 @@
{{end}}
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
- {{if $inlineDiff.EscapeStatus.Escaped}} {{end}}
+ {{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $inlineDiff.EscapeStatus}}
{{- if and $canCreateComment -}}
@@ -77,7 +77,7 @@
{{- svg "octicon-plus" -}}
{{- end -}}
- {{$inlineDiff.Content}}
+ {{$inlineDiff.Content}}
{{if $line.Comments}}
diff --git a/templates/repo/diff/escape_title.tmpl b/templates/repo/diff/escape_title.tmpl
deleted file mode 100644
index 9787ae1d42dfc..0000000000000
--- a/templates/repo/diff/escape_title.tmpl
+++ /dev/null
@@ -1,2 +0,0 @@
-{{if .diff.EscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end -}}
-{{- if .diff.EscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}
diff --git a/templates/repo/diff/section_code.tmpl b/templates/repo/diff/section_code.tmpl
index 3e8303eda66a5..3b9ed0c88cbe0 100644
--- a/templates/repo/diff/section_code.tmpl
+++ b/templates/repo/diff/section_code.tmpl
@@ -1 +1 @@
-{{.diff.Content}}
+{{.diff.Content}}
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index ab23b1b934b7b..c13b205518e47 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -18,14 +18,14 @@
{{if eq .GetType 4}}
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
{{$line.RenderBlobExcerptButtons $file.NameHash $diffBlobExcerptData}}
- {{if $inlineDiff.EscapeStatus.Escaped}} {{end}}
+ {{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $inlineDiff.EscapeStatus}}
{{template "repo/diff/section_code" dict "diff" $inlineDiff}}
{{else if and (eq .GetType 3) $hasmatch}}{{/* DEL */}}
{{$match := index $section.Lines $line.Match}}
{{- $leftDiff := ""}}{{if $line.LeftIdx}}{{$leftDiff = $section.GetComputedInlineDiffFor $line ctx.Locale}}{{end}}
{{- $rightDiff := ""}}{{if $match.RightIdx}}{{$rightDiff = $section.GetComputedInlineDiffFor $match ctx.Locale}}{{end}}
- {{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}} {{end}}{{end}}
+ {{if $line.LeftIdx}}{{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $leftDiff.EscapeStatus}}{{end}}
{{- if and $.root.SignedUserID $.root.PageIsPullFiles -}}
@@ -40,7 +40,7 @@
{{- end -}}
- {{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}} {{end}}{{end}}
+ {{if $match.RightIdx}}{{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $rightDiff.EscapeStatus}}{{end}}
{{if $match.RightIdx}} {{end}}
{{- if and $.root.SignedUserID $.root.PageIsPullFiles -}}
@@ -57,7 +57,7 @@
{{else}}
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
- {{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}} {{end}}{{end}}
+ {{if $line.LeftIdx}}{{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $inlineDiff.EscapeStatus}}{{end}}
{{if $line.LeftIdx}} {{end}}
{{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2)) -}}
@@ -72,7 +72,7 @@
{{- end -}}
- {{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}} {{end}}{{end}}
+ {{if $line.RightIdx}}{{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $inlineDiff.EscapeStatus}}{{end}}
{{if $line.RightIdx}} {{end}}
{{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3)) -}}
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index 6776198b7573e..4dee648cddd7a 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -24,9 +24,7 @@
{{end}}
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale -}}
- {{- if $inlineDiff.EscapeStatus.Escaped -}}
-
- {{- end -}}
+ {{ctx.RenderUtils.RenderUnicodeEscapeToggleButton $inlineDiff.EscapeStatus}}
{{if eq .GetType 4}}
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index d91235e0ea8aa..72b5a83b65be4 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -109,9 +109,7 @@
{{$line := Eval $idx "+" 1}}
- {{if $.EscapeStatus.Escaped}}
- {{if (index $.LineEscapeStatus $idx).Escaped}} {{end}}
- {{end}}
+ {{ctx.RenderUtils.RenderUnicodeEscapeToggleTd $.EscapeStatus (index $.LineEscapeStatus $idx)}}
{{$code}}
{{end}}
diff --git a/web_src/css/base.css b/web_src/css/base.css
index bb16b9fe21973..a8d9dea2a250c 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -17,6 +17,7 @@
/* images */
--checkbox-mask-checked: url('data:image/svg+xml;utf8, ');
--checkbox-mask-indeterminate: url('data:image/svg+xml;utf8, ');
+ --octicon-alert-fill: url('data:image/svg+xml;utf8, ');
--octicon-chevron-right: url('data:image/svg+xml;utf8, ');
--octicon-x: url('data:image/svg+xml;utf8, ');
--select-arrows: url('data:image/svg+xml;utf8, ');
@@ -686,6 +687,7 @@ overflow-menu .ui.label {
}
.lines-num,
+.lines-escape,
.lines-code {
font-size: 12px;
font-family: var(--fonts-monospace);
diff --git a/web_src/css/modules/charescape.css b/web_src/css/modules/charescape.css
index 0c9cbb55b5ecc..35439ebc27b3f 100644
--- a/web_src/css/modules/charescape.css
+++ b/web_src/css/modules/charescape.css
@@ -1,24 +1,31 @@
/*
Show the escaped and hide the real char:
- {real-char}
+ {real-char}
Only show the real-char:
{real-char}
*/
-.broken-code-point:not([data-escaped]),
-.broken-code-point[data-escaped]::before {
+.broken-code-point:not([data-escaped]) {
border-radius: 4px;
padding: 0 2px;
- color: var(--color-body);
- background: var(--color-text-light-1);
+ border: 1px var(--color-yellow) solid;
+}
+
+.broken-code-point[data-escaped] {
+ position: relative;
}
.broken-code-point[data-escaped]::before {
visibility: visible;
content: attr(data-escaped);
+ border-radius: 2px;
+ padding: 0 1px;
+ border: 1px var(--color-yellow) solid;
}
+
.broken-code-point[data-escaped] .char {
- /* make it copyable by selecting the text (AI suggestion, no other solution) */
+ /* keep the original character selectable/copyable while showing the escaped label via ::before */
position: absolute;
+ left: 0;
opacity: 0;
pointer-events: none;
}
@@ -26,11 +33,11 @@ Only show the real-char:
/*
Show the escaped and hide the real-char:
- {real-char}
+ {real-char}
Hide the escaped and show the real-char:
- {real-char}
+ {real-char}
*/
.unicode-escaped .escaped-code-point[data-escaped]::before {
diff --git a/web_src/css/review.css b/web_src/css/review.css
index 9e320346d86a0..f192b0322be15 100644
--- a/web_src/css/review.css
+++ b/web_src/css/review.css
@@ -15,11 +15,20 @@
transform: scale(1.1);
}
+.lines-escape .toggle-escape-button {
+ margin: -1px 2px 0;
+}
+
.lines-escape .toggle-escape-button::before {
- visibility: visible;
- content: "โ ๏ธ";
- font-family: var(--fonts-emoji);
- color: var(--color-red);
+ content: "";
+ display: inline-flex;
+ width: 14px;
+ height: 14px;
+ background-color: var(--color-yellow); /* TODO: maybe it needs a new kind of color, there is no suitable "warning" color in the current palette */
+ mask-image: var(--octicon-alert-fill);
+ -webkit-mask-image: var(--octicon-alert-fill);
+ mask-size: contain;
+ -webkit-mask-size: contain;
}
.repository .diff-file-box .code-diff td.lines-escape {