From 1e96a85f1b67f967d727126a9534469e26b6ec2a Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 2 Apr 2026 21:27:42 +0200 Subject: [PATCH] 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) --- modules/charset/escape_stream.go | 10 +++++++++- modules/charset/escape_test.go | 2 +- modules/highlight/highlight.go | 18 +++++------------- modules/highlight/highlight_test.go | 8 ++++---- web_src/css/modules/charescape.css | 10 +++++----- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 22e7f14f39..98a25e9e55 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -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 } diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index 9d796a0c18..d79a4df6c6 100644 --- a/modules/charset/escape_test.go +++ b/modules/charset/escape_test.go @@ -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(``+"\u001e"+``, test.result) + test.result = addPrefix(``+"\u001e"+``, test.result) test.status.Escaped = true test.status.HasInvisible = true tests = append(tests, test) diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index addc372f85..35d041527a 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -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(`` + string(byte(i)) + ``) } - // 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)) + ``) + // DEL (0x7F) maps to U+2421 + globalVarsPtr.escCtrlCharsMap[0x7f] = template.HTML(`` + string(byte(0x7f)) + ``) globalVarsPtr.escCtrlCharsMap['\t'] = "" globalVarsPtr.escCtrlCharsMap['\n'] = "" globalVarsPtr.escCtrlCharsMap['\r'] = "" diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go index cad22ba9bb..2bd298e780 100644 --- a/modules/highlight/highlight_test.go +++ b/modules/highlight/highlight_test.go @@ -206,12 +206,12 @@ func TestUnsafeSplitHighlightedLines(t *testing.T) { } 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")) + 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]) + assert.Equal(t, template.HTML(`# `+string(byte(0x7f))+`<>`), out[0]) out = renderPlainText([]byte("# \x7f<>")) - assert.Equal(t, template.HTML(`# `+string(byte(0x7f))+`<>`), out[0]) + assert.Equal(t, template.HTML(`# `+string(byte(0x7f))+`<>`), out[0]) } diff --git a/web_src/css/modules/charescape.css b/web_src/css/modules/charescape.css index 0c9cbb55b5..0bbd0dd573 100644 --- a/web_src/css/modules/charescape.css +++ b/web_src/css/modules/charescape.css @@ -1,11 +1,10 @@ /* 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); @@ -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: - {real-char} + {real-char} Hide the escaped and show the real-char: - {real-char} + {real-char} */ .unicode-escaped .escaped-code-point[data-escaped]::before {