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.

Split filePreviewPatternProcessor into a new type FilePreview and some functions to make code more maintainable

+276 -238
+269
modules/markup/file_preview.go
··· 1 + package markup 2 + 3 + import ( 4 + "bytes" 5 + "html/template" 6 + "regexp" 7 + "slices" 8 + "strconv" 9 + "strings" 10 + 11 + "code.gitea.io/gitea/modules/charset" 12 + "code.gitea.io/gitea/modules/setting" 13 + "code.gitea.io/gitea/modules/translation" 14 + "golang.org/x/net/html" 15 + "golang.org/x/net/html/atom" 16 + ) 17 + 18 + var ( 19 + // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" 20 + filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) 21 + ) 22 + 23 + type FilePreview struct { 24 + fileContent []template.HTML 25 + subTitle template.HTML 26 + lineOffset int 27 + urlFull string 28 + filePath string 29 + start int 30 + end int 31 + } 32 + 33 + func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview { 34 + preview := &FilePreview{} 35 + 36 + m := filePreviewPattern.FindStringSubmatchIndex(node.Data) 37 + if m == nil { 38 + return nil 39 + } 40 + 41 + // Ensure that every group has a match 42 + if slices.Contains(m, -1) { 43 + return nil 44 + } 45 + 46 + preview.urlFull = node.Data[m[0]:m[1]] 47 + 48 + // Ensure that we only use links to local repositories 49 + if !strings.HasPrefix(preview.urlFull, setting.AppURL+setting.AppSubURL) { 50 + return nil 51 + } 52 + 53 + projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/") 54 + 55 + commitSha := node.Data[m[4]:m[5]] 56 + preview.filePath = node.Data[m[6]:m[7]] 57 + hash := node.Data[m[8]:m[9]] 58 + 59 + preview.start = m[0] 60 + preview.end = m[1] 61 + 62 + // If url ends in '.', it's very likely that it is not part of the 63 + // actual url but used to finish a sentence. 64 + if strings.HasSuffix(preview.urlFull, ".") { 65 + preview.end-- 66 + preview.urlFull = preview.urlFull[:len(preview.urlFull)-1] 67 + hash = hash[:len(hash)-1] 68 + } 69 + 70 + projPathSegments := strings.Split(projPath, "/") 71 + fileContent, err := DefaultProcessorHelper.GetRepoFileContent( 72 + ctx.Ctx, 73 + projPathSegments[len(projPathSegments)-2], 74 + projPathSegments[len(projPathSegments)-1], 75 + commitSha, preview.filePath, 76 + ) 77 + if err != nil { 78 + return nil 79 + } 80 + 81 + lineSpecs := strings.Split(hash, "-") 82 + lineCount := len(fileContent) 83 + 84 + commitLinkBuffer := new(bytes.Buffer) 85 + html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) 86 + 87 + if len(lineSpecs) == 1 { 88 + line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 89 + if line < 1 || line > lineCount { 90 + return nil 91 + } 92 + 93 + preview.fileContent = fileContent[line-1 : line] 94 + preview.subTitle = locale.Tr( 95 + "markup.filepreview.line", line, 96 + template.HTML(commitLinkBuffer.String()), 97 + ) 98 + 99 + preview.lineOffset = line - 1 100 + } else { 101 + startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 102 + endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) 103 + 104 + if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { 105 + return nil 106 + } 107 + 108 + preview.fileContent = fileContent[startLine-1 : endLine] 109 + preview.subTitle = locale.Tr( 110 + "markup.filepreview.lines", startLine, endLine, 111 + template.HTML(commitLinkBuffer.String()), 112 + ) 113 + 114 + preview.lineOffset = startLine - 1 115 + } 116 + 117 + return preview 118 + } 119 + 120 + func (p *FilePreview) CreateHtml(locale translation.Locale) *html.Node { 121 + table := &html.Node{ 122 + Type: html.ElementNode, 123 + Data: atom.Table.String(), 124 + Attr: []html.Attribute{{Key: "class", Val: "file-preview"}}, 125 + } 126 + tbody := &html.Node{ 127 + Type: html.ElementNode, 128 + Data: atom.Tbody.String(), 129 + } 130 + 131 + status := &charset.EscapeStatus{} 132 + statuses := make([]*charset.EscapeStatus, len(p.fileContent)) 133 + for i, line := range p.fileContent { 134 + statuses[i], p.fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext) 135 + status = status.Or(statuses[i]) 136 + } 137 + 138 + for idx, code := range p.fileContent { 139 + tr := &html.Node{ 140 + Type: html.ElementNode, 141 + Data: atom.Tr.String(), 142 + } 143 + 144 + lineNum := strconv.Itoa(p.lineOffset + idx + 1) 145 + 146 + tdLinesnum := &html.Node{ 147 + Type: html.ElementNode, 148 + Data: atom.Td.String(), 149 + Attr: []html.Attribute{ 150 + {Key: "id", Val: "L" + lineNum}, 151 + {Key: "class", Val: "lines-num"}, 152 + }, 153 + } 154 + spanLinesNum := &html.Node{ 155 + Type: html.ElementNode, 156 + Data: atom.Span.String(), 157 + Attr: []html.Attribute{ 158 + {Key: "id", Val: "L" + lineNum}, 159 + {Key: "data-line-number", Val: lineNum}, 160 + }, 161 + } 162 + tdLinesnum.AppendChild(spanLinesNum) 163 + tr.AppendChild(tdLinesnum) 164 + 165 + if status.Escaped { 166 + tdLinesEscape := &html.Node{ 167 + Type: html.ElementNode, 168 + Data: atom.Td.String(), 169 + Attr: []html.Attribute{ 170 + {Key: "class", Val: "lines-escape"}, 171 + }, 172 + } 173 + 174 + if statuses[idx].Escaped { 175 + btnTitle := "" 176 + if statuses[idx].HasInvisible { 177 + btnTitle += locale.TrString("repo.invisible_runes_line") + " " 178 + } 179 + if statuses[idx].HasAmbiguous { 180 + btnTitle += locale.TrString("repo.ambiguous_runes_line") 181 + } 182 + 183 + escapeBtn := &html.Node{ 184 + Type: html.ElementNode, 185 + Data: atom.Button.String(), 186 + Attr: []html.Attribute{ 187 + {Key: "class", Val: "toggle-escape-button btn interact-bg"}, 188 + {Key: "title", Val: btnTitle}, 189 + }, 190 + } 191 + tdLinesEscape.AppendChild(escapeBtn) 192 + } 193 + 194 + tr.AppendChild(tdLinesEscape) 195 + } 196 + 197 + tdCode := &html.Node{ 198 + Type: html.ElementNode, 199 + Data: atom.Td.String(), 200 + Attr: []html.Attribute{ 201 + {Key: "rel", Val: "L" + lineNum}, 202 + {Key: "class", Val: "lines-code chroma"}, 203 + }, 204 + } 205 + codeInner := &html.Node{ 206 + Type: html.ElementNode, 207 + Data: atom.Code.String(), 208 + Attr: []html.Attribute{{Key: "class", Val: "code-inner"}}, 209 + } 210 + codeText := &html.Node{ 211 + Type: html.RawNode, 212 + Data: string(code), 213 + } 214 + codeInner.AppendChild(codeText) 215 + tdCode.AppendChild(codeInner) 216 + tr.AppendChild(tdCode) 217 + 218 + tbody.AppendChild(tr) 219 + } 220 + 221 + table.AppendChild(tbody) 222 + 223 + twrapper := &html.Node{ 224 + Type: html.ElementNode, 225 + Data: atom.Div.String(), 226 + Attr: []html.Attribute{{Key: "class", Val: "ui table"}}, 227 + } 228 + twrapper.AppendChild(table) 229 + 230 + header := &html.Node{ 231 + Type: html.ElementNode, 232 + Data: atom.Div.String(), 233 + Attr: []html.Attribute{{Key: "class", Val: "header"}}, 234 + } 235 + afilepath := &html.Node{ 236 + Type: html.ElementNode, 237 + Data: atom.A.String(), 238 + Attr: []html.Attribute{ 239 + {Key: "href", Val: p.urlFull}, 240 + {Key: "class", Val: "muted"}, 241 + }, 242 + } 243 + afilepath.AppendChild(&html.Node{ 244 + Type: html.TextNode, 245 + Data: p.filePath, 246 + }) 247 + header.AppendChild(afilepath) 248 + 249 + psubtitle := &html.Node{ 250 + Type: html.ElementNode, 251 + Data: atom.Span.String(), 252 + Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, 253 + } 254 + psubtitle.AppendChild(&html.Node{ 255 + Type: html.RawNode, 256 + Data: string(p.subTitle), 257 + }) 258 + header.AppendChild(psubtitle) 259 + 260 + preview_node := &html.Node{ 261 + Type: html.ElementNode, 262 + Data: atom.Div.String(), 263 + Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, 264 + } 265 + preview_node.AppendChild(header) 266 + preview_node.AppendChild(twrapper) 267 + 268 + return preview_node 269 + }
+7 -238
modules/markup/html.go
··· 5 5 6 6 import ( 7 7 "bytes" 8 - "html/template" 9 8 "io" 10 9 "net/url" 11 10 "path" 12 11 "path/filepath" 13 12 "regexp" 14 - "slices" 15 - "strconv" 16 13 "strings" 17 14 "sync" 18 15 19 16 "code.gitea.io/gitea/modules/base" 20 - "code.gitea.io/gitea/modules/charset" 21 17 "code.gitea.io/gitea/modules/emoji" 22 18 "code.gitea.io/gitea/modules/git" 23 19 "code.gitea.io/gitea/modules/log" ··· 64 60 comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) 65 61 66 62 validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) 67 - 68 - // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" 69 - filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) 70 63 71 64 // While this email regex is definitely not perfect and I'm sure you can come up 72 65 // with edge cases, it is still accepted by the CommonMark specification, as ··· 1072 1065 1073 1066 next := node.NextSibling 1074 1067 for node != nil && node != next { 1075 - m := filePreviewPattern.FindStringSubmatchIndex(node.Data) 1076 - if m == nil { 1077 - return 1078 - } 1079 - 1080 - // Ensure that every group has a match 1081 - if slices.Contains(m, -1) { 1082 - return 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 := strings.TrimSuffix(node.Data[m[2]:m[3]], "/") 1093 - 1094 - commitSha := node.Data[m[4]:m[5]] 1095 - filePath := node.Data[m[6]:m[7]] 1096 - hash := node.Data[m[8]:m[9]] 1097 - 1098 - start := m[0] 1099 - end := m[1] 1100 - 1101 - // If url ends in '.', it's very likely that it is not part of the 1102 - // actual url but used to finish a sentence. 1103 - if strings.HasSuffix(urlFull, ".") { 1104 - end-- 1105 - urlFull = urlFull[:len(urlFull)-1] 1106 - hash = hash[:len(hash)-1] 1107 - } 1108 - 1109 - projPathSegments := strings.Split(projPath, "/") 1110 - fileContent, err := DefaultProcessorHelper.GetRepoFileContent( 1111 - ctx.Ctx, 1112 - projPathSegments[len(projPathSegments)-2], 1113 - projPathSegments[len(projPathSegments)-1], 1114 - commitSha, filePath, 1115 - ) 1116 - if err != nil { 1117 - return 1118 - } 1119 - 1120 - lineSpecs := strings.Split(hash, "-") 1121 - lineCount := len(fileContent) 1122 - 1123 - commitLinkBuffer := new(bytes.Buffer) 1124 - html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) 1125 - 1126 - var subTitle template.HTML 1127 - var lineOffset int 1128 - 1129 1068 locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale) 1130 1069 if !ok { 1131 1070 locale = translation.NewLocale("en-US") 1132 1071 } 1133 1072 1134 - if len(lineSpecs) == 1 { 1135 - line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 1136 - if line < 1 || line > lineCount { 1137 - return 1138 - } 1139 - 1140 - fileContent = fileContent[line-1 : line] 1141 - subTitle = locale.Tr( 1142 - "markup.filepreview.line", line, 1143 - template.HTML(commitLinkBuffer.String()), 1144 - ) 1145 - 1146 - lineOffset = line - 1 1147 - } else { 1148 - startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 1149 - endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) 1150 - 1151 - if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { 1152 - return 1153 - } 1154 - 1155 - fileContent = fileContent[startLine-1 : endLine] 1156 - subTitle = locale.Tr( 1157 - "markup.filepreview.lines", startLine, endLine, 1158 - template.HTML(commitLinkBuffer.String()), 1159 - ) 1160 - 1161 - lineOffset = startLine - 1 1162 - } 1163 - 1164 - table := &html.Node{ 1165 - Type: html.ElementNode, 1166 - Data: atom.Table.String(), 1167 - Attr: []html.Attribute{{Key: "class", Val: "file-preview"}}, 1168 - } 1169 - tbody := &html.Node{ 1170 - Type: html.ElementNode, 1171 - Data: atom.Tbody.String(), 1172 - } 1173 - 1174 - status := &charset.EscapeStatus{} 1175 - statuses := make([]*charset.EscapeStatus, len(fileContent)) 1176 - for i, line := range fileContent { 1177 - statuses[i], fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext) 1178 - status = status.Or(statuses[i]) 1179 - } 1180 - 1181 - for idx, code := range fileContent { 1182 - tr := &html.Node{ 1183 - Type: html.ElementNode, 1184 - Data: atom.Tr.String(), 1185 - } 1186 - 1187 - lineNum := strconv.Itoa(lineOffset + idx + 1) 1188 - 1189 - tdLinesnum := &html.Node{ 1190 - Type: html.ElementNode, 1191 - Data: atom.Td.String(), 1192 - Attr: []html.Attribute{ 1193 - {Key: "id", Val: "L" + lineNum}, 1194 - {Key: "class", Val: "lines-num"}, 1195 - }, 1196 - } 1197 - spanLinesNum := &html.Node{ 1198 - Type: html.ElementNode, 1199 - Data: atom.Span.String(), 1200 - Attr: []html.Attribute{ 1201 - {Key: "id", Val: "L" + lineNum}, 1202 - {Key: "data-line-number", Val: lineNum}, 1203 - }, 1204 - } 1205 - tdLinesnum.AppendChild(spanLinesNum) 1206 - tr.AppendChild(tdLinesnum) 1207 - 1208 - if status.Escaped { 1209 - tdLinesEscape := &html.Node{ 1210 - Type: html.ElementNode, 1211 - Data: atom.Td.String(), 1212 - Attr: []html.Attribute{ 1213 - {Key: "class", Val: "lines-escape"}, 1214 - }, 1215 - } 1216 - 1217 - if statuses[idx].Escaped { 1218 - btnTitle := "" 1219 - if statuses[idx].HasInvisible { 1220 - btnTitle += locale.TrString("repo.invisible_runes_line") + " " 1221 - } 1222 - if statuses[idx].HasAmbiguous { 1223 - btnTitle += locale.TrString("repo.ambiguous_runes_line") 1224 - } 1225 - 1226 - escapeBtn := &html.Node{ 1227 - Type: html.ElementNode, 1228 - Data: atom.Button.String(), 1229 - Attr: []html.Attribute{ 1230 - {Key: "class", Val: "toggle-escape-button btn interact-bg"}, 1231 - {Key: "title", Val: btnTitle}, 1232 - }, 1233 - } 1234 - tdLinesEscape.AppendChild(escapeBtn) 1235 - } 1236 - 1237 - tr.AppendChild(tdLinesEscape) 1238 - } 1239 - 1240 - tdCode := &html.Node{ 1241 - Type: html.ElementNode, 1242 - Data: atom.Td.String(), 1243 - Attr: []html.Attribute{ 1244 - {Key: "rel", Val: "L" + lineNum}, 1245 - {Key: "class", Val: "lines-code chroma"}, 1246 - }, 1247 - } 1248 - codeInner := &html.Node{ 1249 - Type: html.ElementNode, 1250 - Data: atom.Code.String(), 1251 - Attr: []html.Attribute{{Key: "class", Val: "code-inner"}}, 1252 - } 1253 - codeText := &html.Node{ 1254 - Type: html.RawNode, 1255 - Data: string(code), 1256 - } 1257 - codeInner.AppendChild(codeText) 1258 - tdCode.AppendChild(codeInner) 1259 - tr.AppendChild(tdCode) 1260 - 1261 - tbody.AppendChild(tr) 1073 + preview := NewFilePreview(ctx, node, locale) 1074 + if preview == nil { 1075 + return 1262 1076 } 1263 1077 1264 - table.AppendChild(tbody) 1265 - 1266 - twrapper := &html.Node{ 1267 - Type: html.ElementNode, 1268 - Data: atom.Div.String(), 1269 - Attr: []html.Attribute{{Key: "class", Val: "ui table"}}, 1270 - } 1271 - twrapper.AppendChild(table) 1272 - 1273 - header := &html.Node{ 1274 - Type: html.ElementNode, 1275 - Data: atom.Div.String(), 1276 - Attr: []html.Attribute{{Key: "class", Val: "header"}}, 1277 - } 1278 - afilepath := &html.Node{ 1279 - Type: html.ElementNode, 1280 - Data: atom.A.String(), 1281 - Attr: []html.Attribute{ 1282 - {Key: "href", Val: urlFull}, 1283 - {Key: "class", Val: "muted"}, 1284 - }, 1285 - } 1286 - afilepath.AppendChild(&html.Node{ 1287 - Type: html.TextNode, 1288 - Data: filePath, 1289 - }) 1290 - header.AppendChild(afilepath) 1291 - 1292 - psubtitle := &html.Node{ 1293 - Type: html.ElementNode, 1294 - Data: atom.Span.String(), 1295 - Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, 1296 - } 1297 - psubtitle.AppendChild(&html.Node{ 1298 - Type: html.RawNode, 1299 - Data: string(subTitle), 1300 - }) 1301 - header.AppendChild(psubtitle) 1302 - 1303 - preview := &html.Node{ 1304 - Type: html.ElementNode, 1305 - Data: atom.Div.String(), 1306 - Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, 1307 - } 1308 - preview.AppendChild(header) 1309 - preview.AppendChild(twrapper) 1078 + preview_node := preview.CreateHtml(locale) 1310 1079 1311 1080 // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div 1312 - before := node.Data[:start] 1313 - after := node.Data[end:] 1081 + before := node.Data[:preview.start] 1082 + after := node.Data[preview.end:] 1314 1083 node.Data = before 1315 1084 nextSibling := node.NextSibling 1316 1085 node.Parent.InsertBefore(&html.Node{ 1317 1086 Type: html.RawNode, 1318 1087 Data: "</p>", 1319 1088 }, nextSibling) 1320 - node.Parent.InsertBefore(preview, nextSibling) 1089 + node.Parent.InsertBefore(preview_node, nextSibling) 1321 1090 node.Parent.InsertBefore(&html.Node{ 1322 1091 Type: html.RawNode, 1323 1092 Data: "<p>" + after,