Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion modules/charset/escape_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ import (
// VScode defaultWordRegexp
var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`)

// ControlCharPicture returns the Unicode Control Picture for ASCII control
// characters (0x00-0x1F → U+2400-U+241F, 0x7F → U+2421). For other runes it
// returns 0, false.
func ControlCharPicture(r rune) (rune, bool) {
if r >= 0 && r <= 0x1f {
return 0x2400 + r, true
}
if r == 0x7f {
return 0x2421, true
}
return 0, false
}

func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer {
allowedM := make(map[rune]bool, len(allowed))
for _, v := range allowed {
Expand Down Expand Up @@ -199,12 +212,19 @@ func (e *escapeStreamer) invisibleRune(r rune) error {
e.escaped.Escaped = true
e.escaped.HasInvisible = true

var escaped string
if pic, ok := ControlCharPicture(r); ok {
escaped = string(pic)
} else {
escaped = fmt.Sprintf("[U+%04X]", r)
}

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
}
Expand Down
2 changes: 1 addition & 1 deletion modules/charset/escape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 14 additions & 22 deletions modules/highlight/highlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"slices"
"sync"

"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -43,20 +44,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",
for i := range 0x20 {
pic, _ := charset.ControlCharPicture(rune(i))
globalVarsPtr.escCtrlCharsMap[i] = controlCharHTML(pic, 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(`<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>`)
pic, _ := charset.ControlCharPicture(0x7f)
globalVarsPtr.escCtrlCharsMap[0x7f] = controlCharHTML(pic, 0x7f)
globalVarsPtr.escCtrlCharsMap['\t'] = ""
globalVarsPtr.escCtrlCharsMap['\n'] = ""
globalVarsPtr.escCtrlCharsMap['\r'] = ""
Expand All @@ -72,6 +65,10 @@ func globalVars() *globalVarsType {
return globalVarsPtr
}

func controlCharHTML(pic rune, char byte) template.HTML {
return template.HTML(`<span class="broken-code-point" data-escaped="` + string(pic) + `"><span class="char">` + string(char) + `</span></span>`)
}

func escapeByMap(code []byte, escapeMap []template.HTML) template.HTML {
firstEscapePos := -1
for i, c := range code {
Expand Down Expand Up @@ -175,11 +172,7 @@ func RenderCodeByLexer(lexer chroma.Lexer, code string) template.HTML {
return escapeFullString(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 escapeControlChars(htmlBuf.Bytes())
}

// RenderFullFile returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
Expand All @@ -191,10 +184,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(lineBytes)
}
return lines, lexerName
}
Expand Down
8 changes: 4 additions & 4 deletions modules/highlight/highlight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
2 changes: 2 additions & 0 deletions web_src/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
/* images */
--checkbox-mask-checked: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="9" viewBox="0 0 12 9"><path fill-rule="evenodd" d="M11.78.22a.75.75 0 0 1 0 1.061L4.52 8.541a.75.75 0 0 1-1.062 0L.202 5.285a.75.75 0 0 1 1.061-1.061l2.725 2.723L10.718.22a.75.75 0 0 1 1.062 0"/></svg>');
--checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="2" viewBox="0 0 10 2"><path fill-rule="evenodd" d="M0 1a1 1 0 0 1 1-1h8a1 1 0 1 1 0 2H1a1 1 0 0 1-1-1" clip-rule="evenodd"/></svg>');
--octicon-alert-fill: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575ZM8 5a.75.75 0 0 0-.75.75v2.5a.75.75 0 0 0 1.5 0v-2.5A.75.75 0 0 0 8 5Zm1 6a1 1 0 1 0-2 0 1 1 0 0 0 2 0Z"/></svg>');
--octicon-chevron-right: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"></path></svg>');
--octicon-x: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.75.75 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.75.75 0 0 1-.734-.215L8 9.06l-3.22 3.22a.75.75 0 0 1-1.042-.018.75.75 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06"/></svg>');
--select-arrows: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="m4.074 9.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.043 9H4.251a.25.25 0 0 0-.177.427m0-1.957L7.47 4.073a.25.25 0 0 1 .354 0L11.22 7.47a.25.25 0 0 1-.177.426H4.251a.25.25 0 0 1-.177-.426"/></svg>');
Expand Down Expand Up @@ -686,6 +687,7 @@ overflow-menu .ui.label {
}

.lines-num,
.lines-escape,
.lines-code {
font-size: 12px;
font-family: var(--fonts-monospace);
Expand Down
21 changes: 15 additions & 6 deletions web_src/css/modules/charescape.css
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
/*
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);
background: var(--color-text-light-1);
}

.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;
color: var(--color-body);
background: var(--color-text-light-1);
}

.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;
}

/*
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 {
Expand Down
18 changes: 15 additions & 3 deletions web_src/css/review.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,23 @@
transform: scale(1.1);
}

.lines-escape .toggle-escape-button {
padding: 2px;
transform: translateY(-1px);
}

.lines-escape .toggle-escape-button::before {
visibility: visible;
content: "⚠️";
font-family: var(--fonts-emoji);
color: var(--color-red);
content: "";
display: inline-block;
width: 14px;
height: 14px;
vertical-align: middle;
background-color: var(--color-yellow);
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 {
Expand Down