0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-09 09:57:34 +02:00

Use Unicode Control Pictures for control character display

Render ASCII control characters (0x00-0x1F, 0x7F) as Unicode Control
Pictures (U+2400-U+2421) instead of text abbreviations like [DEL] or
[U+001E]. This applies to both the file view and diff view paths.
Also style control char badges in red without background, matching
the style of other escaped code points.

Co-Authored-By: Claude (Opus 4.6) <noreply@anthropic.com>
This commit is contained in:
silverwind 2026-04-02 21:27:42 +02:00
parent 30c07c20e9
commit 1e96a85f1b
No known key found for this signature in database
GPG Key ID: 2E62B41C93869443
5 changed files with 24 additions and 24 deletions

View File

@ -199,12 +199,20 @@ func (e *escapeStreamer) invisibleRune(r rune) error {
e.escaped.Escaped = true
e.escaped.HasInvisible = true
// Use Unicode Control Pictures for ASCII control chars
escaped := fmt.Sprintf("[U+%04X]", r)
if r >= 0 && r <= 0x1f {
escaped = string(0x2400 + r)
} else if r == 0x7f {
escaped = string(rune(0x2421))
}
if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{
Key: "class",
Val: "escaped-code-point",
}, html.Attribute{
Key: "data-escaped",
Val: fmt.Sprintf("[U+%04X]", r),
Val: escaped,
}); err != nil {
return err
}

View File

@ -151,7 +151,7 @@ func TestEscapeControlReader(t *testing.T) {
for _, test := range escapeControlTests {
test.name += " (+Control)"
test.text = addPrefix("\u001E", test.text)
test.result = addPrefix(`<span class="escaped-code-point" data-escaped="[U+001E]"><span class="char">`+"\u001e"+`</span></span>`, test.result)
test.result = addPrefix(`<span class="escaped-code-point" data-escaped="`+string(rune(0x241e))+`"><span class="char">`+"\u001e"+`</span></span>`, test.result)
test.status.Escaped = true
test.status.HasInvisible = true
tests = append(tests, test)

View File

@ -43,20 +43,12 @@ func globalVars() *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",
// ASCII control characters 0x00-0x1F map to Unicode Control Pictures U+2400-U+241F
for i := range 0x20 {
globalVarsPtr.escCtrlCharsMap[i] = template.HTML(`<span class="broken-code-point" data-escaped="` + string(rune(0x2400+i)) + `"><span class="char">` + string(byte(i)) + `</span></span>`)
}
// 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(`<span class="broken-code-point" data-escaped="` + s + `"><span class="char">` + string(byte(i)) + `</span></span>`)
}
globalVarsPtr.escCtrlCharsMap[0x7f] = template.HTML(`<span class="broken-code-point" data-escaped="DEL"><span class="char">` + string(byte(0x7f)) + `</span></span>`)
// DEL (0x7F) maps to U+2421
globalVarsPtr.escCtrlCharsMap[0x7f] = template.HTML(`<span class="broken-code-point" data-escaped="` + string(rune(0x2421)) + `"><span class="char">` + string(byte(0x7f)) + `</span></span>`)
globalVarsPtr.escCtrlCharsMap['\t'] = ""
globalVarsPtr.escCtrlCharsMap['\n'] = ""
globalVarsPtr.escCtrlCharsMap['\r'] = ""

View File

@ -206,12 +206,12 @@ func TestUnsafeSplitHighlightedLines(t *testing.T) {
}
func TestEscape(t *testing.T) {
assert.Equal(t, template.HTML("\t\r\n<span class=\"broken-code-point\" data-escaped=\"NUL\"><span class=\"char\">\x00</span></span><span class=\"broken-code-point\" data-escaped=\"US\"><span class=\"char\">\x1f</span></span>&'\"<>"), escapeControlChars([]byte("\t\r\n\x00\x1f&'\"<>")))
assert.Equal(t, template.HTML("<span class=\"broken-code-point\" data-escaped=\"NUL\"><span class=\"char\">\x00</span></span><span class=\"broken-code-point\" data-escaped=\"US\"><span class=\"char\">\x1f</span></span>&amp;&#39;&#34;&lt;&gt;\t\r\n"), escapeFullString("\x00\x1f&'\"<>\t\r\n"))
assert.Equal(t, template.HTML("\t\r\n<span class=\"broken-code-point\" data-escaped=\"\u2400\"><span class=\"char\">\x00</span></span><span class=\"broken-code-point\" data-escaped=\"\u241f\"><span class=\"char\">\x1f</span></span>&'\"<>"), escapeControlChars([]byte("\t\r\n\x00\x1f&'\"<>")))
assert.Equal(t, template.HTML("<span class=\"broken-code-point\" data-escaped=\"\u2400\"><span class=\"char\">\x00</span></span><span class=\"broken-code-point\" data-escaped=\"\u241f\"><span class=\"char\">\x1f</span></span>&amp;&#39;&#34;&lt;&gt;\t\r\n"), escapeFullString("\x00\x1f&'\"<>\t\r\n"))
out, _ := RenderFullFile("a.py", "", []byte("# \x7f<>"))
assert.Equal(t, template.HTML(`<span class="c1"># <span class="broken-code-point" data-escaped="DEL"><span class="char">`+string(byte(0x7f))+`</span></span>&lt;&gt;</span>`), out[0])
assert.Equal(t, template.HTML(`<span class="c1"># <span class="broken-code-point" data-escaped="`+string(rune(0x2421))+`"><span class="char">`+string(byte(0x7f))+`</span></span>&lt;&gt;</span>`), out[0])
out = renderPlainText([]byte("# \x7f<>"))
assert.Equal(t, template.HTML(`# <span class="broken-code-point" data-escaped="DEL"><span class="char">`+string(byte(0x7f))+`</span></span>&lt;&gt;`), out[0])
assert.Equal(t, template.HTML(`# <span class="broken-code-point" data-escaped="`+string(rune(0x2421))+`"><span class="char">`+string(byte(0x7f))+`</span></span>&lt;&gt;`), out[0])
}

View File

@ -1,11 +1,10 @@
/*
Show the escaped and hide the real char:
<span class="broken-code-point" data-escaped="DEL"><span class="char">{real-char}</span></span>
<span class="broken-code-point" data-escaped=""><span class="char">{real-char}</span></span>
Only show the real-char:
<span class="broken-code-point">{real-char}</span>
*/
.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);
@ -15,6 +14,7 @@ Only show the real-char:
.broken-code-point[data-escaped]::before {
visibility: visible;
content: attr(data-escaped);
color: var(--color-red);
}
.broken-code-point[data-escaped] .char {
/* make it copyable by selecting the text (AI suggestion, no other solution) */
@ -26,11 +26,11 @@ Only show the real-char:
/*
Show the escaped and hide the real-char:
<span class="unicode-escaped">
<span class="escaped-code-point" data-escaped="U+1F600"><span class="char">{real-char}</span></span>
<span class="escaped-code-point" data-escaped="[U+1F600]"><span class="char">{real-char}</span></span>
</span>
Hide the escaped and show the real-char:
<span>
<span class="escaped-code-point" data-escaped="U+1F600"><span class="char">{real-char}</span></span>
<span class="escaped-code-point" data-escaped="[U+1F600]"><span class="char">{real-char}</span></span>
</span>
*/
.unicode-escaped .escaped-code-point[data-escaped]::before {