loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Render inline file permalinks

+468 -4
+267
modules/markup/html.go
··· 10 10 "path" 11 11 "path/filepath" 12 12 "regexp" 13 + "strconv" 13 14 "strings" 14 15 "sync" 15 16 16 17 "code.gitea.io/gitea/modules/base" 18 + "code.gitea.io/gitea/modules/charset" 17 19 "code.gitea.io/gitea/modules/emoji" 18 20 "code.gitea.io/gitea/modules/git" 19 21 "code.gitea.io/gitea/modules/log" ··· 60 62 comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) 61 63 62 64 validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) 65 + 66 + // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" 67 + filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{7,64})/(\S+)#(L\d+(?:-L\d+)?)`) 63 68 64 69 // While this email regex is definitely not perfect and I'm sure you can come up 65 70 // with edge cases, it is still accepted by the CommonMark specification, as ··· 171 176 var defaultProcessors = []processor{ 172 177 fullIssuePatternProcessor, 173 178 comparePatternProcessor, 179 + filePreviewPatternProcessor, 174 180 fullHashPatternProcessor, 175 181 shortLinkProcessor, 176 182 linkProcessor, ··· 1051 1057 } 1052 1058 replaceContent(node, start, end, createCodeLink(urlFull, text, "compare")) 1053 1059 node = node.NextSibling.NextSibling 1060 + } 1061 + } 1062 + 1063 + func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { 1064 + if ctx.Metas == nil { 1065 + return 1066 + } 1067 + if DefaultProcessorHelper.GetRepoFileContent == nil || DefaultProcessorHelper.GetLocale == nil { 1068 + return 1069 + } 1070 + 1071 + next := node.NextSibling 1072 + for node != nil && node != next { 1073 + m := filePreviewPattern.FindStringSubmatchIndex(node.Data) 1074 + if m == nil { 1075 + return 1076 + } 1077 + 1078 + // Ensure that every group (m[0]...m[9]) has a match 1079 + for i := 0; i < 10; i++ { 1080 + if m[i] == -1 { 1081 + return 1082 + } 1083 + } 1084 + 1085 + urlFull := node.Data[m[0]:m[1]] 1086 + 1087 + // Ensure that we only use links to local repositories 1088 + if !strings.HasPrefix(urlFull, setting.AppURL+setting.AppSubURL) { 1089 + return 1090 + } 1091 + 1092 + projPath := node.Data[m[2]:m[3]] 1093 + projPath = strings.TrimSuffix(projPath, "/") 1094 + 1095 + commitSha := node.Data[m[4]:m[5]] 1096 + filePath := node.Data[m[6]:m[7]] 1097 + hash := node.Data[m[8]:m[9]] 1098 + 1099 + start := m[0] 1100 + end := m[1] 1101 + 1102 + // If url ends in '.', it's very likely that it is not part of the 1103 + // actual url but used to finish a sentence. 1104 + if strings.HasSuffix(urlFull, ".") { 1105 + end-- 1106 + urlFull = urlFull[:len(urlFull)-1] 1107 + hash = hash[:len(hash)-1] 1108 + } 1109 + 1110 + projPathSegments := strings.Split(projPath, "/") 1111 + fileContent, err := DefaultProcessorHelper.GetRepoFileContent( 1112 + ctx.Ctx, 1113 + projPathSegments[len(projPathSegments)-2], 1114 + projPathSegments[len(projPathSegments)-1], 1115 + commitSha, filePath, 1116 + ) 1117 + if err != nil { 1118 + return 1119 + } 1120 + 1121 + lineSpecs := strings.Split(hash, "-") 1122 + lineCount := len(fileContent) 1123 + 1124 + var subTitle string 1125 + var lineOffset int 1126 + 1127 + if len(lineSpecs) == 1 { 1128 + line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 1129 + if line < 1 || line > lineCount { 1130 + return 1131 + } 1132 + 1133 + fileContent = fileContent[line-1 : line] 1134 + subTitle = "Line " + strconv.Itoa(line) 1135 + 1136 + lineOffset = line - 1 1137 + } else { 1138 + startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 1139 + endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) 1140 + 1141 + if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { 1142 + return 1143 + } 1144 + 1145 + fileContent = fileContent[startLine-1 : endLine] 1146 + subTitle = "Lines " + strconv.Itoa(startLine) + " to " + strconv.Itoa(endLine) 1147 + 1148 + lineOffset = startLine - 1 1149 + } 1150 + 1151 + table := &html.Node{ 1152 + Type: html.ElementNode, 1153 + Data: atom.Table.String(), 1154 + Attr: []html.Attribute{{Key: "class", Val: "file-preview"}}, 1155 + } 1156 + tbody := &html.Node{ 1157 + Type: html.ElementNode, 1158 + Data: atom.Tbody.String(), 1159 + } 1160 + 1161 + locale, err := DefaultProcessorHelper.GetLocale(ctx.Ctx) 1162 + if err != nil { 1163 + log.Error("Unable to get locale. Error: %v", err) 1164 + return 1165 + } 1166 + 1167 + status := &charset.EscapeStatus{} 1168 + statuses := make([]*charset.EscapeStatus, len(fileContent)) 1169 + for i, line := range fileContent { 1170 + statuses[i], fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext) 1171 + status = status.Or(statuses[i]) 1172 + } 1173 + 1174 + for idx, code := range fileContent { 1175 + tr := &html.Node{ 1176 + Type: html.ElementNode, 1177 + Data: atom.Tr.String(), 1178 + } 1179 + 1180 + lineNum := strconv.Itoa(lineOffset + idx + 1) 1181 + 1182 + tdLinesnum := &html.Node{ 1183 + Type: html.ElementNode, 1184 + Data: atom.Td.String(), 1185 + Attr: []html.Attribute{ 1186 + {Key: "id", Val: "L" + lineNum}, 1187 + {Key: "class", Val: "lines-num"}, 1188 + }, 1189 + } 1190 + spanLinesNum := &html.Node{ 1191 + Type: html.ElementNode, 1192 + Data: atom.Span.String(), 1193 + Attr: []html.Attribute{ 1194 + {Key: "id", Val: "L" + lineNum}, 1195 + {Key: "data-line-number", Val: lineNum}, 1196 + }, 1197 + } 1198 + tdLinesnum.AppendChild(spanLinesNum) 1199 + tr.AppendChild(tdLinesnum) 1200 + 1201 + if status.Escaped { 1202 + tdLinesEscape := &html.Node{ 1203 + Type: html.ElementNode, 1204 + Data: atom.Td.String(), 1205 + Attr: []html.Attribute{ 1206 + {Key: "class", Val: "lines-escape"}, 1207 + }, 1208 + } 1209 + 1210 + if statuses[idx].Escaped { 1211 + btnTitle := "" 1212 + if statuses[idx].HasInvisible { 1213 + btnTitle += locale.TrString("repo.invisible_runes_line") + " " 1214 + } 1215 + if statuses[idx].HasAmbiguous { 1216 + btnTitle += locale.TrString("repo.ambiguous_runes_line") 1217 + } 1218 + 1219 + escapeBtn := &html.Node{ 1220 + Type: html.ElementNode, 1221 + Data: atom.Button.String(), 1222 + Attr: []html.Attribute{ 1223 + {Key: "class", Val: "toggle-escape-button btn interact-bg"}, 1224 + {Key: "title", Val: btnTitle}, 1225 + }, 1226 + } 1227 + tdLinesEscape.AppendChild(escapeBtn) 1228 + } 1229 + 1230 + tr.AppendChild(tdLinesEscape) 1231 + } 1232 + 1233 + tdCode := &html.Node{ 1234 + Type: html.ElementNode, 1235 + Data: atom.Td.String(), 1236 + Attr: []html.Attribute{ 1237 + {Key: "rel", Val: "L" + lineNum}, 1238 + {Key: "class", Val: "lines-code chroma"}, 1239 + }, 1240 + } 1241 + codeInner := &html.Node{ 1242 + Type: html.ElementNode, 1243 + Data: atom.Code.String(), 1244 + Attr: []html.Attribute{{Key: "class", Val: "code-inner"}}, 1245 + } 1246 + codeText := &html.Node{ 1247 + Type: html.RawNode, 1248 + Data: string(code), 1249 + } 1250 + codeInner.AppendChild(codeText) 1251 + tdCode.AppendChild(codeInner) 1252 + tr.AppendChild(tdCode) 1253 + 1254 + tbody.AppendChild(tr) 1255 + } 1256 + 1257 + table.AppendChild(tbody) 1258 + 1259 + twrapper := &html.Node{ 1260 + Type: html.ElementNode, 1261 + Data: atom.Div.String(), 1262 + Attr: []html.Attribute{{Key: "class", Val: "ui table"}}, 1263 + } 1264 + twrapper.AppendChild(table) 1265 + 1266 + header := &html.Node{ 1267 + Type: html.ElementNode, 1268 + Data: atom.Div.String(), 1269 + Attr: []html.Attribute{{Key: "class", Val: "header"}}, 1270 + } 1271 + afilepath := &html.Node{ 1272 + Type: html.ElementNode, 1273 + Data: atom.A.String(), 1274 + Attr: []html.Attribute{ 1275 + {Key: "href", Val: urlFull}, 1276 + {Key: "class", Val: "muted"}, 1277 + }, 1278 + } 1279 + afilepath.AppendChild(&html.Node{ 1280 + Type: html.TextNode, 1281 + Data: filePath, 1282 + }) 1283 + header.AppendChild(afilepath) 1284 + 1285 + psubtitle := &html.Node{ 1286 + Type: html.ElementNode, 1287 + Data: atom.Span.String(), 1288 + Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, 1289 + } 1290 + psubtitle.AppendChild(&html.Node{ 1291 + Type: html.TextNode, 1292 + Data: subTitle + " in ", 1293 + }) 1294 + psubtitle.AppendChild(createLink(urlFull[m[0]:m[5]], commitSha[0:7], "text black")) 1295 + header.AppendChild(psubtitle) 1296 + 1297 + preview := &html.Node{ 1298 + Type: html.ElementNode, 1299 + Data: atom.Div.String(), 1300 + Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, 1301 + } 1302 + preview.AppendChild(header) 1303 + preview.AppendChild(twrapper) 1304 + 1305 + // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div 1306 + before := node.Data[:start] 1307 + after := node.Data[end:] 1308 + node.Data = before 1309 + nextSibling := node.NextSibling 1310 + node.Parent.InsertBefore(&html.Node{ 1311 + Type: html.RawNode, 1312 + Data: "</p>", 1313 + }, nextSibling) 1314 + node.Parent.InsertBefore(preview, nextSibling) 1315 + node.Parent.InsertBefore(&html.Node{ 1316 + Type: html.RawNode, 1317 + Data: "<p>" + after, 1318 + }, nextSibling) 1319 + 1320 + node = node.NextSibling 1054 1321 } 1055 1322 } 1056 1323
+57
modules/markup/html_test.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "html/template" 8 9 "io" 9 10 "os" 10 11 "strings" ··· 13 14 "code.gitea.io/gitea/models/unittest" 14 15 "code.gitea.io/gitea/modules/emoji" 15 16 "code.gitea.io/gitea/modules/git" 17 + "code.gitea.io/gitea/modules/highlight" 16 18 "code.gitea.io/gitea/modules/log" 17 19 "code.gitea.io/gitea/modules/markup" 18 20 "code.gitea.io/gitea/modules/markup/markdown" 19 21 "code.gitea.io/gitea/modules/setting" 22 + "code.gitea.io/gitea/modules/translation" 20 23 "code.gitea.io/gitea/modules/util" 21 24 22 25 "github.com/stretchr/testify/assert" ··· 673 676 assert.NoError(t, err) 674 677 assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String()) 675 678 } 679 + 680 + func TestRender_FilePreview(t *testing.T) { 681 + setting.AppURL = markup.TestAppURL 682 + markup.Init(&markup.ProcessorHelper{ 683 + GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { 684 + buf := []byte("A\nB\nC\nD\n") 685 + return highlight.PlainText(buf), nil 686 + }, 687 + GetLocale: func(ctx context.Context) (translation.Locale, error) { 688 + return translation.NewLocale("en-US"), nil 689 + }, 690 + }) 691 + 692 + sha := "b6dd6210eaebc915fd5be5579c58cce4da2e2579" 693 + commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L1-L2" 694 + 695 + test := func(input, expected string) { 696 + buffer, err := markup.RenderString(&markup.RenderContext{ 697 + Ctx: git.DefaultContext, 698 + RelativePath: ".md", 699 + Metas: localMetas, 700 + }, input) 701 + assert.NoError(t, err) 702 + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) 703 + } 704 + 705 + test( 706 + commitFilePreview, 707 + `<p></p>`+ 708 + `<div class="file-preview-box">`+ 709 + `<div class="header">`+ 710 + `<a href="http://localhost:3000/gogits/gogs/src/commit/b6dd6210eaebc915fd5be5579c58cce4da2e2579/path/to/file.go#L1-L2" class="muted" rel="nofollow">path/to/file.go</a>`+ 711 + `<span class="text small grey">`+ 712 + `Lines 1 to 2 in <a href="http://localhost:3000/gogits/gogs/src/commit/b6dd6210eaebc915fd5be5579c58cce4da2e2579" class="text black" rel="nofollow">b6dd621</a>`+ 713 + `</span>`+ 714 + `</div>`+ 715 + `<div class="ui table">`+ 716 + `<table class="file-preview">`+ 717 + `<tbody>`+ 718 + `<tr>`+ 719 + `<td id="user-content-L1" class="lines-num"><span id="user-content-L1" data-line-number="1"></span></td>`+ 720 + `<td rel="L1" class="lines-code chroma"><code class="code-inner">A`+"\n"+`</code></td>`+ 721 + `</tr>`+ 722 + `<tr>`+ 723 + `<td id="user-content-L2" class="lines-num"><span id="user-content-L2" data-line-number="2"></span></td>`+ 724 + `<td rel="L2" class="lines-code chroma"><code class="code-inner">B`+"\n"+`</code></td>`+ 725 + `</tr>`+ 726 + `</tbody>`+ 727 + `</table>`+ 728 + `</div>`+ 729 + `</div>`+ 730 + `<p></p>`, 731 + ) 732 + }
+4
modules/markup/renderer.go
··· 8 8 "context" 9 9 "errors" 10 10 "fmt" 11 + "html/template" 11 12 "io" 12 13 "net/url" 13 14 "path/filepath" ··· 16 17 17 18 "code.gitea.io/gitea/modules/git" 18 19 "code.gitea.io/gitea/modules/setting" 20 + "code.gitea.io/gitea/modules/translation" 19 21 "code.gitea.io/gitea/modules/util" 20 22 21 23 "github.com/yuin/goldmark/ast" ··· 31 33 32 34 type ProcessorHelper struct { 33 35 IsUsernameMentionable func(ctx context.Context, username string) bool 36 + GetRepoFileContent func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) 37 + GetLocale func(ctx context.Context) (translation.Locale, error) 34 38 35 39 ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute 36 40 }
+17
modules/markup/sanitizer.go
··· 120 120 // Allow 'color' and 'background-color' properties for the style attribute on text elements. 121 121 policy.AllowStyles("color", "background-color").OnElements("span", "p") 122 122 123 + // Allow classes for file preview links... 124 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^(lines-num|lines-code chroma)$")).OnElements("td") 125 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^code-inner$")).OnElements("code") 126 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview-box$")).OnElements("div") 127 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui table$")).OnElements("div") 128 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") 129 + policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span") 130 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") 131 + policy.AllowAttrs("rel").Matching(regexp.MustCompile("^L[0-9]+$")).OnElements("td") 132 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") 133 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") 134 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") 135 + policy.AllowAttrs("title").OnElements("button") 136 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") 137 + policy.AllowAttrs("data-tooltip-content").OnElements("span") 138 + policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") 139 + 123 140 // Allow generally safe attributes 124 141 generalSafeAttrs := []string{ 125 142 "abbr", "accept", "accept-charset",
+81
services/markup/processorhelper.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "fmt" 9 + "html/template" 10 + "io" 8 11 12 + "code.gitea.io/gitea/models/perm/access" 13 + "code.gitea.io/gitea/models/repo" 14 + "code.gitea.io/gitea/models/unit" 9 15 "code.gitea.io/gitea/models/user" 16 + "code.gitea.io/gitea/modules/gitrepo" 17 + "code.gitea.io/gitea/modules/highlight" 18 + "code.gitea.io/gitea/modules/log" 10 19 "code.gitea.io/gitea/modules/markup" 20 + "code.gitea.io/gitea/modules/translation" 11 21 gitea_context "code.gitea.io/gitea/services/context" 22 + file_service "code.gitea.io/gitea/services/repository/files" 12 23 ) 13 24 14 25 func ProcessorHelper() *markup.ProcessorHelper { ··· 28 39 29 40 // when using gitea context (web context), use user's visibility and user's permission to check 30 41 return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer) 42 + }, 43 + GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { 44 + repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) 45 + if err != nil { 46 + return nil, err 47 + } 48 + 49 + var user *user.User 50 + 51 + giteaCtx, ok := ctx.(*gitea_context.Context) 52 + if ok { 53 + user = giteaCtx.Doer 54 + } 55 + 56 + perms, err := access.GetUserRepoPermission(ctx, repo, user) 57 + if err != nil { 58 + return nil, err 59 + } 60 + if !perms.CanRead(unit.TypeCode) { 61 + return nil, fmt.Errorf("cannot access repository code") 62 + } 63 + 64 + gitRepo, err := gitrepo.OpenRepository(ctx, repo) 65 + if err != nil { 66 + return nil, err 67 + } 68 + 69 + commit, err := gitRepo.GetCommit(commitSha) 70 + if err != nil { 71 + return nil, err 72 + } 73 + 74 + language, err := file_service.TryGetContentLanguage(gitRepo, commitSha, filePath) 75 + if err != nil { 76 + log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err) 77 + } 78 + 79 + blob, err := commit.GetBlobByPath(filePath) 80 + if err != nil { 81 + return nil, err 82 + } 83 + 84 + dataRc, err := blob.DataAsync() 85 + if err != nil { 86 + return nil, err 87 + } 88 + defer dataRc.Close() 89 + 90 + buf, _ := io.ReadAll(dataRc) 91 + 92 + fileContent, _, err := highlight.File(blob.Name(), language, buf) 93 + if err != nil { 94 + log.Error("highlight.File failed, fallback to plain text: %v", err) 95 + fileContent = highlight.PlainText(buf) 96 + } 97 + 98 + return fileContent, nil 99 + }, 100 + GetLocale: func(ctx context.Context) (translation.Locale, error) { 101 + giteaCtx, ok := ctx.(*gitea_context.Context) 102 + if ok { 103 + return giteaCtx.Locale, nil 104 + } 105 + 106 + giteaBaseCtx, ok := ctx.(*gitea_context.Base) 107 + if ok { 108 + return giteaBaseCtx.Locale, nil 109 + } 110 + 111 + return nil, fmt.Errorf("could not retrieve locale from context") 31 112 }, 32 113 } 33 114 }
+1
web_src/css/index.css
··· 30 30 @import "./markup/content.css"; 31 31 @import "./markup/codecopy.css"; 32 32 @import "./markup/asciicast.css"; 33 + @import "./markup/filepreview.css"; 33 34 34 35 @import "./chroma/base.css"; 35 36 @import "./codemirror/base.css";
+2 -1
web_src/css/markup/content.css
··· 451 451 text-decoration: inherit; 452 452 } 453 453 454 - .markup pre > code { 454 + .markup pre > code, 455 + .markup .file-preview code { 455 456 padding: 0; 456 457 margin: 0; 457 458 font-size: 100%;
+35
web_src/css/markup/filepreview.css
··· 1 + .markup table.file-preview { 2 + margin-bottom: 0; 3 + } 4 + 5 + .markup table.file-preview td { 6 + padding: 0 10px !important; 7 + border: none !important; 8 + } 9 + 10 + .markup table.file-preview tr { 11 + border-top: none; 12 + background-color: inherit !important; 13 + } 14 + 15 + .markup .file-preview-box { 16 + margin-bottom: 16px; 17 + } 18 + 19 + .markup .file-preview-box .header { 20 + padding: .5rem; 21 + padding-left: 1rem; 22 + border: 1px solid var(--color-secondary); 23 + border-bottom: none; 24 + border-radius: 0.28571429rem 0.28571429rem 0 0; 25 + background: var(--color-box-header); 26 + } 27 + 28 + .markup .file-preview-box .header > a { 29 + display: block; 30 + } 31 + 32 + .markup .file-preview-box .table { 33 + margin-top: 0; 34 + border-radius: 0 0 0.28571429rem 0.28571429rem; 35 + }
+2 -1
web_src/css/repo/linebutton.css
··· 1 - .code-view .lines-num:hover { 1 + .code-view .lines-num:hover, 2 + .file-preview .lines-num:hover { 2 3 color: var(--color-text-dark) !important; 3 4 } 4 5
+2 -2
web_src/js/features/repo-unicode-escape.js
··· 7 7 8 8 e.preventDefault(); 9 9 10 - const fileContent = btn.closest('.file-content, .non-diff-file-content'); 11 - const fileView = fileContent?.querySelectorAll('.file-code, .file-view'); 10 + const fileContent = btn.closest('.file-content, .non-diff-file-content, .file-preview-box'); 11 + const fileView = fileContent?.querySelectorAll('.file-code, .file-view, .file-preview'); 12 12 if (btn.matches('.escape-button')) { 13 13 for (const el of fileView) el.classList.add('unicode-escaped'); 14 14 hideElem(btn);