@@ -25,9 +25,6 @@ const (
2525 IssueNameStyleRegexp = "regexp"
2626)
2727
28- // CSS class for action keywords (e.g. "closes: #1")
29- const keywordClass = "issue-keyword"
30-
3128type globalVarsType struct {
3229 hashCurrentPattern * regexp.Regexp
3330 shortLinkPattern * regexp.Regexp
@@ -39,6 +36,7 @@ type globalVarsType struct {
3936 emojiShortCodeRegex * regexp.Regexp
4037 issueFullPattern * regexp.Regexp
4138 filesChangedFullPattern * regexp.Regexp
39+ codePreviewPattern * regexp.Regexp
4240
4341 tagCleaner * regexp.Regexp
4442 nulCleaner * strings.Replacer
@@ -88,6 +86,9 @@ var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
8886 // example: https://domain/org/repo/pulls/27/files#hash
8987 v .filesChangedFullPattern = regexp .MustCompile (`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b` )
9088
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+
9192 v .tagCleaner = regexp .MustCompile (`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))` )
9293 v .nulCleaner = strings .NewReplacer ("\000 " , "" )
9394 return v
@@ -164,11 +165,7 @@ var defaultProcessors = []processor{
164165// emails with HTML links, parsing shortlinks in the format of [[Link]], like
165166// MediaWiki, linking issues in the format #ID, and mentions in the format
166167// @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 {
172169 return postProcess (ctx , defaultProcessors , input , output )
173170}
174171
@@ -189,10 +186,7 @@ var commitMessageProcessors = []processor{
189186// RenderCommitMessage will use the same logic as PostProcess, but will disable
190187// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
191188// 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 ) {
196190 procs := commitMessageProcessors
197191 return renderProcessString (ctx , procs , content )
198192}
@@ -219,10 +213,7 @@ var emojiProcessors = []processor{
219213// RenderCommitMessage, but will disable the shortLinkProcessor and
220214// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
221215// 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 ) {
226217 procs := slices .Clone (commitMessageSubjectProcessors )
227218 procs = append (procs , func (ctx * RenderContext , node * html.Node ) {
228219 ch := & html.Node {Parent : node , Type : html .TextNode , Data : node .Data }
@@ -236,10 +227,7 @@ func RenderCommitMessageSubject(
236227}
237228
238229// 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 ) {
243231 // do not render other issue/commit links in an issue's title - which in most cases is already a link.
244232 return renderProcessString (ctx , []processor {
245233 emojiShortCodeProcessor ,
@@ -257,10 +245,7 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string)
257245
258246// RenderDescriptionHTML will use similar logic as PostProcess, but will
259247// 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 ) {
264249 return renderProcessString (ctx , []processor {
265250 descriptionLinkProcessor ,
266251 emojiShortCodeProcessor ,
@@ -270,10 +255,7 @@ func RenderDescriptionHTML(
270255
271256// RenderEmoji for when we want to just process emoji and shortcodes
272257// 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 ) {
277259 return renderProcessString (ctx , emojiProcessors , content )
278260}
279261
@@ -333,6 +315,17 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
333315 return nil
334316}
335317
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+
336329func visitNode (ctx * RenderContext , procs []processor , node * html.Node ) * html.Node {
337330 // Add user-content- to IDs and "#" links if they don't already have them
338331 for idx , attr := range node .Attr {
@@ -346,47 +339,27 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
346339 if attr .Key == "href" && strings .HasPrefix (attr .Val , "#" ) && notHasPrefix {
347340 node .Attr [idx ].Val = "#user-content-" + val
348341 }
349-
350- if attr .Key == "class" && attr .Val == "emoji" {
351- procs = nil
352- }
353342 }
354343
355344 switch node .Type {
356345 case html .TextNode :
357- processTextNodes (ctx , procs , node )
346+ for _ , proc := range procs {
347+ proc (ctx , node ) // it might add siblings
348+ }
349+
358350 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
361354 return node .NextSibling
355+ } else if node .Data == "code" || node .Data == "pre" {
356+ return node .NextSibling // ignore code and pre nodes
362357 } else if node .Data == "img" {
363358 return visitNodeImg (ctx , node )
364359 } else if node .Data == "video" {
365360 return visitNodeVideo (ctx , node )
366361 } 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
390363 }
391364 for n := node .FirstChild ; n != nil ; {
392365 n = visitNode (ctx , procs , n )
@@ -396,22 +369,17 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
396369 return node .NextSibling
397370}
398371
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-
407372// 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+
409377 span := & html.Node {
410378 Type : html .ElementNode ,
411379 Data : atom .Span .String (),
412380 Attr : []html.Attribute {},
413381 }
414- span .Attr = append (span .Attr , html. Attribute { Key : "class" , Val : keywordClass } )
382+ span .Attr = append (span .Attr , ctx . RenderInternal . NodeSafeAttr ( "class" , keywordClass ) )
415383
416384 text := & html.Node {
417385 Type : html .TextNode ,
@@ -422,7 +390,7 @@ func createKeyword(content string) *html.Node {
422390 return span
423391}
424392
425- func createLink (href , content , class string ) * html.Node {
393+ func createLink (ctx * RenderContext , href , content , class string ) * html.Node {
426394 a := & html.Node {
427395 Type : html .ElementNode ,
428396 Data : atom .A .String (),
@@ -432,7 +400,7 @@ func createLink(href, content, class string) *html.Node {
432400 a .Attr = append (a .Attr , html.Attribute {Key : "data-markdown-generated-content" })
433401 }
434402 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 ) )
436404 }
437405
438406 text := & html.Node {
0 commit comments