Skip to content

Commit 6687faf

Browse files
committed
fix
1 parent 624c0ba commit 6687faf

File tree

5 files changed

+68
-42
lines changed

5 files changed

+68
-42
lines changed

modules/highlight/highlight.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"bytes"
1010
"fmt"
1111
gohtml "html"
12+
"html/template"
1213
"io"
1314
"path/filepath"
1415
"strings"
@@ -135,12 +136,26 @@ func CodeFromLexer(lexer chroma.Lexer, code string) string {
135136
return strings.TrimSuffix(htmlbuf.String(), "\n")
136137
}
137138

139+
type ContentLines struct {
140+
HTMLLines []template.HTML
141+
HasLastEOL bool
142+
LexerName string
143+
}
144+
145+
func (cl *ContentLines) ShouldShowIncompleteMark(idx int) bool {
146+
return !cl.HasLastEOL && idx == len(cl.HTMLLines)-1
147+
}
148+
149+
func hasLastEOL(code []byte) bool {
150+
return len(code) != 0 && code[len(code)-1] == '\n'
151+
}
152+
138153
// File returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
139-
func File(fileName, language string, code []byte) ([]string, string, error) {
154+
func File(fileName, language string, code []byte) (*ContentLines, error) {
140155
NewContext()
141156

142157
if len(code) > sizeLimit {
143-
return PlainText(code), "", nil
158+
return PlainText(code), nil
144159
}
145160

146161
formatter := html.New(html.WithClasses(true),
@@ -177,30 +192,30 @@ func File(fileName, language string, code []byte) ([]string, string, error) {
177192

178193
iterator, err := lexer.Tokenise(nil, string(code))
179194
if err != nil {
180-
return nil, "", fmt.Errorf("can't tokenize code: %w", err)
195+
return nil, fmt.Errorf("can't tokenize code: %w", err)
181196
}
182197

183198
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
184199
htmlBuf := &bytes.Buffer{}
185200

186-
lines := make([]string, 0, len(tokensLines))
201+
lines := make([]template.HTML, 0, len(tokensLines))
187202
for _, tokens := range tokensLines {
188203
iterator = chroma.Literator(tokens...)
189204
err = formatter.Format(htmlBuf, githubStyles, iterator)
190205
if err != nil {
191-
return nil, "", fmt.Errorf("can't format code: %w", err)
206+
return nil, fmt.Errorf("can't format code: %w", err)
192207
}
193-
lines = append(lines, htmlBuf.String())
208+
lines = append(lines, template.HTML(htmlBuf.String()))
194209
htmlBuf.Reset()
195210
}
196211

197-
return lines, lexerName, nil
212+
return &ContentLines{HTMLLines: lines, HasLastEOL: hasLastEOL(code), LexerName: lexerName}, nil
198213
}
199214

200215
// PlainText returns non-highlighted HTML for code
201-
func PlainText(code []byte) []string {
216+
func PlainText(code []byte) *ContentLines {
202217
r := bufio.NewReader(bytes.NewReader(code))
203-
m := make([]string, 0, bytes.Count(code, []byte{'\n'})+1)
218+
m := make([]template.HTML, 0, bytes.Count(code, []byte{'\n'})+1)
204219
for {
205220
content, err := r.ReadString('\n')
206221
if err != nil && err != io.EOF {
@@ -210,10 +225,9 @@ func PlainText(code []byte) []string {
210225
if content == "" && err == io.EOF {
211226
break
212227
}
213-
s := gohtml.EscapeString(content)
214-
m = append(m, s)
228+
m = append(m, template.HTML(gohtml.EscapeString(content)))
215229
}
216-
return m
230+
return &ContentLines{HTMLLines: m, HasLastEOL: hasLastEOL(code)}
217231
}
218232

219233
func formatLexerName(name string) string {

modules/highlight/highlight_test.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package highlight
55

66
import (
7+
"html/template"
78
"strings"
89
"testing"
910

@@ -14,6 +15,16 @@ func lines(s string) []string {
1415
return strings.Split(strings.ReplaceAll(strings.TrimSpace(s), `\n`, "\n"), "\n")
1516
}
1617

18+
func join(lines []template.HTML, sep string) (s string) {
19+
for i, line := range lines {
20+
s += string(line)
21+
if i != len(lines)-1 {
22+
s += sep
23+
}
24+
}
25+
return s
26+
}
27+
1728
func TestFile(t *testing.T) {
1829
tests := []struct {
1930
name string
@@ -97,13 +108,13 @@ c=2
97108

98109
for _, tt := range tests {
99110
t.Run(tt.name, func(t *testing.T) {
100-
out, lexerName, err := File(tt.name, "", []byte(tt.code))
111+
out, err := File(tt.name, "", []byte(tt.code))
101112
assert.NoError(t, err)
102113
expected := strings.Join(tt.want, "\n")
103-
actual := strings.Join(out, "\n")
114+
actual := join(out.HTMLLines, "\n")
104115
assert.Equal(t, strings.Count(actual, "<span"), strings.Count(actual, "</span>"))
105116
assert.EqualValues(t, expected, actual)
106-
assert.Equal(t, tt.lexerName, lexerName)
117+
assert.Equal(t, tt.lexerName, out.LexerName)
107118
})
108119
}
109120
}
@@ -166,7 +177,7 @@ c=2`),
166177
t.Run(tt.name, func(t *testing.T) {
167178
out := PlainText([]byte(tt.code))
168179
expected := strings.Join(tt.want, "\n")
169-
actual := strings.Join(out, "\n")
180+
actual := join(out.HTMLLines, "\n")
170181
assert.EqualValues(t, expected, actual)
171182
})
172183
}

routers/web/repo/view.go

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
gocontext "context"
1010
"encoding/base64"
1111
"fmt"
12+
"html/template"
1213
"image"
1314
"io"
1415
"net/http"
@@ -488,22 +489,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
488489
} else {
489490
buf, _ := io.ReadAll(rd)
490491

491-
// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
492-
// empty: 0 lines; "a": 1 line, 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
493-
// Gitea uses the definition (like most modern editors):
494-
// empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
495-
// When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
496-
// To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
497-
// This NumLines is only used for the display on the UI: "xxx lines"
498-
if len(buf) == 0 {
499-
ctx.Data["NumLines"] = 0
500-
} else {
501-
ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
502-
}
503-
ctx.Data["NumLinesSet"] = true
504-
505-
language := ""
506-
492+
var language string
507493
indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
508494
if err == nil {
509495
defer deleteTemporaryFile()
@@ -527,21 +513,36 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
527513
language = ""
528514
}
529515
}
530-
fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
531-
ctx.Data["LexerName"] = lexerName
516+
fileContentLines, err := highlight.File(blob.Name(), language, buf)
532517
if err != nil {
533518
log.Error("highlight.File failed, fallback to plain text: %v", err)
534-
fileContent = highlight.PlainText(buf)
519+
fileContentLines = highlight.PlainText(buf)
520+
} else {
521+
ctx.Data["LexerName"] = fileContentLines.LexerName // the LexerName field is also used by "blame" page
535522
}
536523
status := &charset.EscapeStatus{}
537-
statuses := make([]*charset.EscapeStatus, len(fileContent))
538-
for i, line := range fileContent {
539-
statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
524+
statuses := make([]*charset.EscapeStatus, len(fileContentLines.HTMLLines))
525+
for i, line := range fileContentLines.HTMLLines {
526+
st, htm := charset.EscapeControlHTML(string(line), ctx.Locale)
527+
statuses[i], fileContentLines.HTMLLines[i] = st, template.HTML(htm)
540528
status = status.Or(statuses[i])
541529
}
542530
ctx.Data["EscapeStatus"] = status
543-
ctx.Data["FileContent"] = fileContent
531+
ctx.Data["FileContentLines"] = fileContentLines
544532
ctx.Data["LineEscapeStatus"] = statuses
533+
534+
// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
535+
// empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
536+
// Gitea uses the definition (like most modern editors):
537+
// empty: 0 lines; "a": 1 line; "a\n": 2 lines (only 1 line is rendered); "a\nb": 2 lines;
538+
// When rendering, the last empty line is not rendered on UI, there is an icon mark to indicate that there is no trailing EOL
539+
// This NumLines is only used for the display purpose on the UI: "xxx lines"
540+
if len(buf) == 0 {
541+
ctx.Data["NumLines"] = 0
542+
} else {
543+
ctx.Data["NumLines"] = len(fileContentLines.HTMLLines)
544+
}
545+
ctx.Data["NumLinesSet"] = true
545546
}
546547
if !fInfo.isLFSFile {
547548
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {

templates/repo/view_file.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@
102102
{{else}}
103103
<table>
104104
<tbody>
105-
{{range $idx, $code := .FileContent}}
105+
{{range $idx, $codeHTML := .FileContentLines.HTMLLines}}
106106
{{$line := Eval $idx "+" 1}}
107107
<tr>
108108
<td id="L{{$line}}" class="lines-num"><span id="L{{$line}}" data-line-number="{{$line}}"></span></td>
109109
{{if $.EscapeStatus.Escaped}}
110110
<td class="lines-escape">{{if (index $.LineEscapeStatus $idx).Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{if (index $.LineEscapeStatus $idx).HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if (index $.LineEscapeStatus $idx).HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></button>{{end}}</td>
111111
{{end}}
112-
<td rel="L{{$line}}" class="lines-code chroma"><code class="code-inner">{{$code | Safe}}</code></td>
112+
<td rel="L{{$line}}" class="lines-code chroma"><code class="code-inner">{{$codeHTML}}</code>{{if $.FileContentLines.ShouldShowIncompleteMark $idx}}<span class="text red unselectable gt-ml-2">🚫</span>{{end}}</td>
113113
</tr>
114114
{{end}}
115115
</tbody>

web_src/js/features/copycontent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function initCopyContent() {
3636
btn.classList.remove('is-loading', 'small-loading-icon');
3737
}
3838
} else { // text, read from DOM
39-
const lineEls = document.querySelectorAll('.file-view .lines-code');
39+
const lineEls = document.querySelectorAll('.file-view .lines-code .code-inner');
4040
content = Array.from(lineEls, (el) => el.textContent).join('');
4141
}
4242

0 commit comments

Comments
 (0)