@@ -25,9 +25,6 @@ const (
25
25
IssueNameStyleRegexp = "regexp"
26
26
)
27
27
28
- // CSS class for action keywords (e.g. "closes: #1")
29
- const keywordClass = "issue-keyword"
30
-
31
28
type globalVarsType struct {
32
29
hashCurrentPattern * regexp.Regexp
33
30
shortLinkPattern * regexp.Regexp
@@ -39,6 +36,7 @@ type globalVarsType struct {
39
36
emojiShortCodeRegex * regexp.Regexp
40
37
issueFullPattern * regexp.Regexp
41
38
filesChangedFullPattern * regexp.Regexp
39
+ codePreviewPattern * regexp.Regexp
42
40
43
41
tagCleaner * regexp.Regexp
44
42
nulCleaner * strings.Replacer
@@ -88,6 +86,9 @@ var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
88
86
// example: https://domain/org/repo/pulls/27/files#hash
89
87
v .filesChangedFullPattern = regexp .MustCompile (`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b` )
90
88
89
+ // codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
90
+ v .codePreviewPattern = regexp .MustCompile (`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)` )
91
+
91
92
v .tagCleaner = regexp .MustCompile (`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))` )
92
93
v .nulCleaner = strings .NewReplacer ("\000 " , "" )
93
94
return v
@@ -164,11 +165,7 @@ var defaultProcessors = []processor{
164
165
// emails with HTML links, parsing shortlinks in the format of [[Link]], like
165
166
// MediaWiki, linking issues in the format #ID, and mentions in the format
166
167
// @user, and others.
167
- func PostProcess (
168
- ctx * RenderContext ,
169
- input io.Reader ,
170
- output io.Writer ,
171
- ) error {
168
+ func PostProcess (ctx * RenderContext , input io.Reader , output io.Writer ) error {
172
169
return postProcess (ctx , defaultProcessors , input , output )
173
170
}
174
171
@@ -189,10 +186,7 @@ var commitMessageProcessors = []processor{
189
186
// RenderCommitMessage will use the same logic as PostProcess, but will disable
190
187
// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
191
188
// set, which changes every text node into a link to the passed default link.
192
- func RenderCommitMessage (
193
- ctx * RenderContext ,
194
- content string ,
195
- ) (string , error ) {
189
+ func RenderCommitMessage (ctx * RenderContext , content string ) (string , error ) {
196
190
procs := commitMessageProcessors
197
191
return renderProcessString (ctx , procs , content )
198
192
}
@@ -219,10 +213,7 @@ var emojiProcessors = []processor{
219
213
// RenderCommitMessage, but will disable the shortLinkProcessor and
220
214
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
221
215
// which changes every text node into a link to the passed default link.
222
- func RenderCommitMessageSubject (
223
- ctx * RenderContext ,
224
- defaultLink , content string ,
225
- ) (string , error ) {
216
+ func RenderCommitMessageSubject (ctx * RenderContext , defaultLink , content string ) (string , error ) {
226
217
procs := slices .Clone (commitMessageSubjectProcessors )
227
218
procs = append (procs , func (ctx * RenderContext , node * html.Node ) {
228
219
ch := & html.Node {Parent : node , Type : html .TextNode , Data : node .Data }
@@ -236,10 +227,7 @@ func RenderCommitMessageSubject(
236
227
}
237
228
238
229
// RenderIssueTitle to process title on individual issue/pull page
239
- func RenderIssueTitle (
240
- ctx * RenderContext ,
241
- title string ,
242
- ) (string , error ) {
230
+ func RenderIssueTitle (ctx * RenderContext , title string ) (string , error ) {
243
231
// do not render other issue/commit links in an issue's title - which in most cases is already a link.
244
232
return renderProcessString (ctx , []processor {
245
233
emojiShortCodeProcessor ,
@@ -257,10 +245,7 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string)
257
245
258
246
// RenderDescriptionHTML will use similar logic as PostProcess, but will
259
247
// use a single special linkProcessor.
260
- func RenderDescriptionHTML (
261
- ctx * RenderContext ,
262
- content string ,
263
- ) (string , error ) {
248
+ func RenderDescriptionHTML (ctx * RenderContext , content string ) (string , error ) {
264
249
return renderProcessString (ctx , []processor {
265
250
descriptionLinkProcessor ,
266
251
emojiShortCodeProcessor ,
@@ -270,10 +255,7 @@ func RenderDescriptionHTML(
270
255
271
256
// RenderEmoji for when we want to just process emoji and shortcodes
272
257
// in various places it isn't already run through the normal markdown processor
273
- func RenderEmoji (
274
- ctx * RenderContext ,
275
- content string ,
276
- ) (string , error ) {
258
+ func RenderEmoji (ctx * RenderContext , content string ) (string , error ) {
277
259
return renderProcessString (ctx , emojiProcessors , content )
278
260
}
279
261
@@ -333,6 +315,17 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
333
315
return nil
334
316
}
335
317
318
+ func isEmojiNode (node * html.Node ) bool {
319
+ if node .Type == html .ElementNode && node .Data == atom .Span .String () {
320
+ for _ , attr := range node .Attr {
321
+ if (attr .Key == "class" || attr .Key == "data-attr-class" ) && strings .Contains (attr .Val , "emoji" ) {
322
+ return true
323
+ }
324
+ }
325
+ }
326
+ return false
327
+ }
328
+
336
329
func visitNode (ctx * RenderContext , procs []processor , node * html.Node ) * html.Node {
337
330
// Add user-content- to IDs and "#" links if they don't already have them
338
331
for idx , attr := range node .Attr {
@@ -346,47 +339,27 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
346
339
if attr .Key == "href" && strings .HasPrefix (attr .Val , "#" ) && notHasPrefix {
347
340
node .Attr [idx ].Val = "#user-content-" + val
348
341
}
349
-
350
- if attr .Key == "class" && attr .Val == "emoji" {
351
- procs = nil
352
- }
353
342
}
354
343
355
344
switch node .Type {
356
345
case html .TextNode :
357
- processTextNodes (ctx , procs , node )
346
+ for _ , proc := range procs {
347
+ proc (ctx , node ) // it might add siblings
348
+ }
349
+
358
350
case html .ElementNode :
359
- if node .Data == "code" || node .Data == "pre" {
360
- // ignore code and pre nodes
351
+ if isEmojiNode (node ) {
352
+ // TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
353
+ // if we don't stop it, it will go into the TextNode again and create an infinite recursion
361
354
return node .NextSibling
355
+ } else if node .Data == "code" || node .Data == "pre" {
356
+ return node .NextSibling // ignore code and pre nodes
362
357
} else if node .Data == "img" {
363
358
return visitNodeImg (ctx , node )
364
359
} else if node .Data == "video" {
365
360
return visitNodeVideo (ctx , node )
366
361
} else if node .Data == "a" {
367
- // Restrict text in links to emojis
368
- procs = emojiProcessors
369
- } else if node .Data == "i" {
370
- for _ , attr := range node .Attr {
371
- if attr .Key != "class" {
372
- continue
373
- }
374
- classes := strings .Split (attr .Val , " " )
375
- for i , class := range classes {
376
- if class == "icon" {
377
- classes [0 ], classes [i ] = classes [i ], classes [0 ]
378
- attr .Val = strings .Join (classes , " " )
379
-
380
- // Remove all children of icons
381
- child := node .FirstChild
382
- for child != nil {
383
- node .RemoveChild (child )
384
- child = node .FirstChild
385
- }
386
- break
387
- }
388
- }
389
- }
362
+ procs = emojiProcessors // Restrict text in links to emojis
390
363
}
391
364
for n := node .FirstChild ; n != nil ; {
392
365
n = visitNode (ctx , procs , n )
@@ -396,22 +369,17 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
396
369
return node .NextSibling
397
370
}
398
371
399
- // processTextNodes runs the passed node through various processors, in order to handle
400
- // all kinds of special links handled by the post-processing.
401
- func processTextNodes (ctx * RenderContext , procs []processor , node * html.Node ) {
402
- for _ , p := range procs {
403
- p (ctx , node )
404
- }
405
- }
406
-
407
372
// createKeyword() renders a highlighted version of an action keyword
408
- func createKeyword (content string ) * html.Node {
373
+ func createKeyword (ctx * RenderContext , content string ) * html.Node {
374
+ // CSS class for action keywords (e.g. "closes: #1")
375
+ const keywordClass = "issue-keyword"
376
+
409
377
span := & html.Node {
410
378
Type : html .ElementNode ,
411
379
Data : atom .Span .String (),
412
380
Attr : []html.Attribute {},
413
381
}
414
- span .Attr = append (span .Attr , html. Attribute { Key : "class" , Val : keywordClass } )
382
+ span .Attr = append (span .Attr , ctx . RenderInternal . NodeSafeAttr ( "class" , keywordClass ) )
415
383
416
384
text := & html.Node {
417
385
Type : html .TextNode ,
@@ -422,7 +390,7 @@ func createKeyword(content string) *html.Node {
422
390
return span
423
391
}
424
392
425
- func createLink (href , content , class string ) * html.Node {
393
+ func createLink (ctx * RenderContext , href , content , class string ) * html.Node {
426
394
a := & html.Node {
427
395
Type : html .ElementNode ,
428
396
Data : atom .A .String (),
@@ -432,7 +400,7 @@ func createLink(href, content, class string) *html.Node {
432
400
a .Attr = append (a .Attr , html.Attribute {Key : "data-markdown-generated-content" })
433
401
}
434
402
if class != "" {
435
- a .Attr = append (a .Attr , html. Attribute { Key : "class" , Val : class } )
403
+ a .Attr = append (a .Attr , ctx . RenderInternal . NodeSafeAttr ( "class" , class ) )
436
404
}
437
405
438
406
text := & html.Node {
0 commit comments