Skip to content

Commit 7190519

Browse files
authored
Fix code highlighting on blame page (#36157)
1. Full file highlighting (fix the legacy todo "we should instead highlight the whole file at once") * Fix #24383 2. Correctly covert file content encoding 3. Remove dead code, split large for-loop into small functions/blocks to make code maintainable
1 parent 1f5237e commit 7190519

File tree

7 files changed

+109
-92
lines changed

7 files changed

+109
-92
lines changed

modules/charset/escape.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ import (
2020
// RuneNBSP is the codepoint for NBSP
2121
const RuneNBSP = 0xa0
2222

23-
// EscapeControlHTML escapes the unicode control sequences in a provided html document
23+
// EscapeControlHTML escapes the Unicode control sequences in a provided html document
2424
func EscapeControlHTML(html template.HTML, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output template.HTML) {
25+
if !setting.UI.AmbiguousUnicodeDetection {
26+
return &EscapeStatus{}, html
27+
}
2528
sb := &strings.Builder{}
2629
escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader
2730
return escaped, template.HTML(sb.String())
2831
}
2932

30-
// EscapeControlReader escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus
33+
// EscapeControlReader escapes the Unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus
3134
func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
3235
if !setting.UI.AmbiguousUnicodeDetection {
3336
_, err = io.Copy(writer, reader)

modules/highlight/highlight.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,39 @@ func NewContext() {
5656
})
5757
}
5858

59-
// Code returns a HTML version of code string with chroma syntax highlighting classes and the matched lexer name
59+
// UnsafeSplitHighlightedLines splits highlighted code into lines preserving HTML tags
60+
// It always includes '\n', '\n' can appear at the end of each line or in the middle of HTML tags
61+
// The '\n' is necessary for copying code from web UI to preserve original code lines
62+
// ATTENTION: It uses the unsafe conversion between string and []byte for performance reason
63+
// DO NOT make any modification to the returned [][]byte slice items
64+
func UnsafeSplitHighlightedLines(code template.HTML) (ret [][]byte) {
65+
buf := util.UnsafeStringToBytes(string(code))
66+
lineCount := bytes.Count(buf, []byte("\n")) + 1
67+
ret = make([][]byte, 0, lineCount)
68+
nlTagClose := []byte("\n</")
69+
for {
70+
pos := bytes.IndexByte(buf, '\n')
71+
if pos == -1 {
72+
if len(buf) > 0 {
73+
ret = append(ret, buf)
74+
}
75+
return ret
76+
}
77+
// Chroma highlighting output sometimes have "</span>" right after \n, sometimes before.
78+
// * "<span>text\n</span>"
79+
// * "<span>text</span>\n"
80+
if bytes.HasPrefix(buf[pos:], nlTagClose) {
81+
pos1 := bytes.IndexByte(buf[pos:], '>')
82+
if pos1 != -1 {
83+
pos += pos1
84+
}
85+
}
86+
ret = append(ret, buf[:pos+1])
87+
buf = buf[pos+1:]
88+
}
89+
}
90+
91+
// Code returns an HTML version of code string with chroma syntax highlighting classes and the matched lexer name
6092
func Code(fileName, language, code string) (output template.HTML, lexerName string) {
6193
NewContext()
6294

modules/highlight/highlight_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,21 @@ c=2`),
181181
})
182182
}
183183
}
184+
185+
func TestUnsafeSplitHighlightedLines(t *testing.T) {
186+
ret := UnsafeSplitHighlightedLines("")
187+
assert.Empty(t, ret)
188+
189+
ret = UnsafeSplitHighlightedLines("a")
190+
assert.Len(t, ret, 1)
191+
assert.Equal(t, "a", string(ret[0]))
192+
193+
ret = UnsafeSplitHighlightedLines("\n")
194+
assert.Len(t, ret, 1)
195+
assert.Equal(t, "\n", string(ret[0]))
196+
197+
ret = UnsafeSplitHighlightedLines("<span>a</span>\n<span>b\n</span>")
198+
assert.Len(t, ret, 2)
199+
assert.Equal(t, "<span>a</span>\n", string(ret[0]))
200+
assert.Equal(t, "<span>b\n</span>", string(ret[1]))
201+
}

routers/web/repo/blame.go

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
package repo
55

66
import (
7+
"bytes"
78
"fmt"
8-
gotemplate "html/template"
9+
"html/template"
910
"net/http"
1011
"net/url"
1112
"path"
@@ -25,18 +26,17 @@ import (
2526
)
2627

2728
type blameRow struct {
28-
RowNumber int
29-
Avatar gotemplate.HTML
30-
RepoLink string
31-
PartSha string
29+
RowNumber int
30+
31+
Avatar template.HTML
3232
PreviousSha string
3333
PreviousShaURL string
34-
IsFirstCommit bool
3534
CommitURL string
3635
CommitMessage string
37-
CommitSince gotemplate.HTML
38-
Code gotemplate.HTML
39-
EscapeStatus *charset.EscapeStatus
36+
CommitSince template.HTML
37+
38+
Code template.HTML
39+
EscapeStatus *charset.EscapeStatus
4040
}
4141

4242
// RefBlame render blame page
@@ -220,76 +220,64 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
220220
return commitNames
221221
}
222222

223-
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
224-
repoLink := ctx.Repo.RepoLink
223+
func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *git.BlamePart, commit *user_model.UserCommit, br *blameRow) {
224+
if commit.User != nil {
225+
br.Avatar = avatarUtils.Avatar(commit.User, 18)
226+
} else {
227+
br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18)
228+
}
229+
230+
br.PreviousSha = part.PreviousSha
231+
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
232+
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
233+
br.CommitMessage = commit.CommitMessage
234+
br.CommitSince = templates.TimeSince(commit.Author.When)
235+
}
225236

237+
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
226238
language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
227239
if err != nil {
228240
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
229241
}
230242

231-
lines := make([]string, 0)
243+
buf := &bytes.Buffer{}
232244
rows := make([]*blameRow, 0)
233-
escapeStatus := &charset.EscapeStatus{}
234-
235-
var lexerName string
236-
237245
avatarUtils := templates.NewAvatarUtils(ctx)
238-
i := 0
239-
commitCnt := 0
246+
rowNumber := 0 // will be 1-based
240247
for _, part := range blameParts {
241-
for index, line := range part.Lines {
242-
i++
243-
lines = append(lines, line)
244-
245-
br := &blameRow{
246-
RowNumber: i,
247-
}
248-
249-
commit := commitNames[part.Sha]
250-
if index == 0 {
251-
// Count commit number
252-
commitCnt++
253-
254-
// User avatar image
255-
commitSince := templates.TimeSince(commit.Author.When)
248+
for partLineIdx, line := range part.Lines {
249+
rowNumber++
256250

257-
var avatar string
258-
if commit.User != nil {
259-
avatar = string(avatarUtils.Avatar(commit.User, 18))
260-
} else {
261-
avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "tw-mr-2"))
262-
}
251+
br := &blameRow{RowNumber: rowNumber}
252+
rows = append(rows, br)
263253

264-
br.Avatar = gotemplate.HTML(avatar)
265-
br.RepoLink = repoLink
266-
br.PartSha = part.Sha
267-
br.PreviousSha = part.PreviousSha
268-
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
269-
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
270-
br.CommitMessage = commit.CommitMessage
271-
br.CommitSince = commitSince
254+
if int64(buf.Len()) < setting.UI.MaxDisplayFileSize {
255+
buf.WriteString(line)
256+
buf.WriteByte('\n')
272257
}
273258

274-
if i != len(lines)-1 {
275-
line += "\n"
259+
if partLineIdx == 0 {
260+
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br)
276261
}
277-
line, lexerNameForLine := highlight.Code(path.Base(ctx.Repo.TreePath), language, line)
262+
}
263+
}
278264

279-
// set lexer name to the first detected lexer. this is certainly suboptimal and
280-
// we should instead highlight the whole file at once
281-
if lexerName == "" {
282-
lexerName = lexerNameForLine
283-
}
265+
escapeStatus := &charset.EscapeStatus{}
284266

285-
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
286-
rows = append(rows, br)
287-
escapeStatus = escapeStatus.Or(br.EscapeStatus)
267+
bufContent := buf.Bytes()
268+
bufContent = charset.ToUTF8(bufContent, charset.ConvertOpts{})
269+
highlighted, lexerName := highlight.Code(path.Base(ctx.Repo.TreePath), language, util.UnsafeBytesToString(bufContent))
270+
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlighted)
271+
for i, br := range rows {
272+
var line template.HTML
273+
if i < len(rows) {
274+
line = template.HTML(util.UnsafeBytesToString(unsafeLines[i]))
288275
}
276+
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
277+
escapeStatus = escapeStatus.Or(br.EscapeStatus)
289278
}
290279

291280
ctx.Data["EscapeStatus"] = escapeStatus
292281
ctx.Data["BlameRows"] = rows
293-
ctx.Data["CommitCnt"] = commitCnt
294282
ctx.Data["LexerName"] = lexerName
295283
}

services/gitdiff/gitdiff.go

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,35 +1336,11 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
13361336
return diff, nil
13371337
}
13381338

1339-
func splitHighlightLines(buf []byte) (ret [][]byte) {
1340-
lineCount := bytes.Count(buf, []byte("\n")) + 1
1341-
ret = make([][]byte, 0, lineCount)
1342-
nlTagClose := []byte("\n</")
1343-
for {
1344-
pos := bytes.IndexByte(buf, '\n')
1345-
if pos == -1 {
1346-
ret = append(ret, buf)
1347-
return ret
1348-
}
1349-
// Chroma highlighting output sometimes have "</span>" right after \n, sometimes before.
1350-
// * "<span>text\n</span>"
1351-
// * "<span>text</span>\n"
1352-
if bytes.HasPrefix(buf[pos:], nlTagClose) {
1353-
pos1 := bytes.IndexByte(buf[pos:], '>')
1354-
if pos1 != -1 {
1355-
pos += pos1
1356-
}
1357-
}
1358-
ret = append(ret, buf[:pos+1])
1359-
buf = buf[pos+1:]
1360-
}
1361-
}
1362-
13631339
func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
13641340
content := util.UnsafeBytesToString(charset.ToUTF8(rawContent, charset.ConvertOpts{}))
13651341
highlightedNewContent, _ := highlight.Code(diffFile.Name, diffFile.Language, content)
1366-
splitLines := splitHighlightLines([]byte(highlightedNewContent))
1367-
lines := make(map[int]template.HTML, len(splitLines))
1342+
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlightedNewContent)
1343+
lines := make(map[int]template.HTML, len(unsafeLines))
13681344
// only save the highlighted lines we need, but not the whole file, to save memory
13691345
for _, sec := range diffFile.Sections {
13701346
for _, ln := range sec.Lines {
@@ -1374,8 +1350,8 @@ func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[
13741350
}
13751351
if lineIdx >= 1 {
13761352
idx := lineIdx - 1
1377-
if idx < len(splitLines) {
1378-
lines[idx] = template.HTML(splitLines[idx])
1353+
if idx < len(unsafeLines) {
1354+
lines[idx] = template.HTML(util.UnsafeBytesToString(unsafeLines[idx]))
13791355
}
13801356
}
13811357
}

templates/repo/blame.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<table>
3939
<tbody>
4040
{{range $row := .BlameRows}}
41-
<tr class="{{if and (gt $.CommitCnt 1) ($row.CommitMessage)}}top-line-blame{{end}}">
41+
<tr class="{{if $row.CommitURL}}top-line-blame{{end}}">
4242
<td class="lines-commit">
4343
<div class="blame-info">
4444
<div class="blame-data">

web_src/css/base.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ overflow-menu .ui.label {
919919
.blame-avatar {
920920
display: flex;
921921
align-items: center;
922-
margin-right: 4px;
922+
margin-right: 6px;
923923
}
924924

925925
tr.top-line-blame {

0 commit comments

Comments
 (0)