@@ -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