@@ -10,10 +10,12 @@ import (
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"
59
61
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
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
64
+ // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2"
65
+ filePreviewPattern = regexp .MustCompile (`https?://((?:\S+/){3})src/commit/([0-9a-f]{7,64})/(\S+)#(L\d+(?:-L\d+)?)` )
66
+
62
67
validLinksPattern = regexp .MustCompile (`^[a-z][\w-]+://` )
63
68
64
69
// While this email regex is definitely not perfect and I'm sure you can come up
@@ -171,6 +176,7 @@ type processor func(ctx *RenderContext, node *html.Node)
171
176
var defaultProcessors = []processor {
172
177
fullIssuePatternProcessor ,
173
178
comparePatternProcessor ,
179
+ filePreviewPatternProcessor ,
174
180
fullHashPatternProcessor ,
175
181
shortLinkProcessor ,
176
182
linkProcessor ,
@@ -1054,6 +1060,266 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
1054
1060
}
1055
1061
}
1056
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
+ return
1164
+ }
1165
+
1166
+ status := & charset.EscapeStatus {}
1167
+ statuses := make ([]* charset.EscapeStatus , len (fileContent ))
1168
+ for i , line := range fileContent {
1169
+ statuses [i ], fileContent [i ] = charset .EscapeControlHTML (line , locale )
1170
+ status = status .Or (statuses [i ])
1171
+ }
1172
+
1173
+ for idx , code := range fileContent {
1174
+ tr := & html.Node {
1175
+ Type : html .ElementNode ,
1176
+ Data : atom .Tr .String (),
1177
+ }
1178
+
1179
+ lineNum := strconv .Itoa (lineOffset + idx + 1 )
1180
+
1181
+ tdLinesnum := & html.Node {
1182
+ Type : html .ElementNode ,
1183
+ Data : atom .Td .String (),
1184
+ Attr : []html.Attribute {
1185
+ {Key : "id" , Val : "L" + lineNum },
1186
+ {Key : "class" , Val : "lines-num" },
1187
+ },
1188
+ }
1189
+ spanLinesNum := & html.Node {
1190
+ Type : html .ElementNode ,
1191
+ Data : atom .Span .String (),
1192
+ Attr : []html.Attribute {
1193
+ {Key : "id" , Val : "L" + lineNum },
1194
+ {Key : "data-line-number" , Val : lineNum },
1195
+ },
1196
+ }
1197
+ tdLinesnum .AppendChild (spanLinesNum )
1198
+ tr .AppendChild (tdLinesnum )
1199
+
1200
+ if status .Escaped {
1201
+ tdLinesEscape := & html.Node {
1202
+ Type : html .ElementNode ,
1203
+ Data : atom .Td .String (),
1204
+ Attr : []html.Attribute {
1205
+ {Key : "class" , Val : "lines-escape" },
1206
+ },
1207
+ }
1208
+
1209
+ if statuses [idx ].Escaped {
1210
+ btnTitle := ""
1211
+ if statuses [idx ].HasInvisible {
1212
+ btnTitle += locale .Tr ("repo.invisible_runes_line" ) + " "
1213
+ }
1214
+ if statuses [idx ].HasAmbiguous {
1215
+ btnTitle += locale .Tr ("repo.ambiguous_runes_line" )
1216
+ }
1217
+
1218
+ escapeBtn := & html.Node {
1219
+ Type : html .ElementNode ,
1220
+ Data : atom .A .String (),
1221
+ Attr : []html.Attribute {
1222
+ {Key : "class" , Val : "toggle-escape-button btn interact-bg" },
1223
+ {Key : "title" , Val : btnTitle },
1224
+ {Key : "href" , Val : "javascript:void(0)" },
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 {{Key : "href" , Val : urlFull }},
1275
+ }
1276
+ afilepath .AppendChild (& html.Node {
1277
+ Type : html .TextNode ,
1278
+ Data : filePath ,
1279
+ })
1280
+ header .AppendChild (afilepath )
1281
+
1282
+ psubtitle := & html.Node {
1283
+ Type : html .ElementNode ,
1284
+ Data : atom .Span .String (),
1285
+ Attr : []html.Attribute {{Key : "class" , Val : "text small grey" }},
1286
+ }
1287
+ psubtitle .AppendChild (& html.Node {
1288
+ Type : html .TextNode ,
1289
+ Data : subTitle + " in " ,
1290
+ })
1291
+ psubtitle .AppendChild (createLink (urlFull [m [0 ]:m [5 ]], commitSha [0 :7 ], "" ))
1292
+ header .AppendChild (psubtitle )
1293
+
1294
+ preview := & html.Node {
1295
+ Type : html .ElementNode ,
1296
+ Data : atom .Div .String (),
1297
+ Attr : []html.Attribute {{Key : "class" , Val : "file-preview-box" }},
1298
+ }
1299
+ preview .AppendChild (header )
1300
+ preview .AppendChild (twrapper )
1301
+
1302
+ // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
1303
+ before := node .Data [:start ]
1304
+ after := node .Data [end :]
1305
+ node .Data = before
1306
+ nextSibling := node .NextSibling
1307
+ node .Parent .InsertBefore (& html.Node {
1308
+ Type : html .RawNode ,
1309
+ Data : "</p>" ,
1310
+ }, nextSibling )
1311
+ node .Parent .InsertBefore (preview , nextSibling )
1312
+ if after != "" {
1313
+ node .Parent .InsertBefore (& html.Node {
1314
+ Type : html .RawNode ,
1315
+ Data : "<p>" + after ,
1316
+ }, nextSibling )
1317
+ }
1318
+
1319
+ node = node .NextSibling
1320
+ }
1321
+ }
1322
+
1057
1323
// emojiShortCodeProcessor for rendering text like :smile: into emoji
1058
1324
func emojiShortCodeProcessor (ctx * RenderContext , node * html.Node ) {
1059
1325
start := 0
0 commit comments