code.gitea.io/gitea@v1.22.3/modules/markup/html.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package markup
     5  
     6  import (
     7  	"bytes"
     8  	"io"
     9  	"net/url"
    10  	"path"
    11  	"path/filepath"
    12  	"regexp"
    13  	"slices"
    14  	"strings"
    15  	"sync"
    16  
    17  	"code.gitea.io/gitea/modules/base"
    18  	"code.gitea.io/gitea/modules/emoji"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/markup/common"
    22  	"code.gitea.io/gitea/modules/references"
    23  	"code.gitea.io/gitea/modules/regexplru"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	"code.gitea.io/gitea/modules/templates/vars"
    26  	"code.gitea.io/gitea/modules/translation"
    27  	"code.gitea.io/gitea/modules/util"
    28  
    29  	"golang.org/x/net/html"
    30  	"golang.org/x/net/html/atom"
    31  	"mvdan.cc/xurls/v2"
    32  )
    33  
    34  // Issue name styles
    35  const (
    36  	IssueNameStyleNumeric      = "numeric"
    37  	IssueNameStyleAlphanumeric = "alphanumeric"
    38  	IssueNameStyleRegexp       = "regexp"
    39  )
    40  
    41  var (
    42  	// NOTE: All below regex matching do not perform any extra validation.
    43  	// Thus a link is produced even if the linked entity does not exist.
    44  	// While fast, this is also incorrect and lead to false positives.
    45  	// TODO: fix invalid linking issue
    46  
    47  	// valid chars in encoded path and parameter: [-+~_%.a-zA-Z0-9/]
    48  
    49  	// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
    50  	// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length
    51  	// so that abbreviated hash links can be used as well. This matches git and GitHub usability.
    52  	hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
    53  
    54  	// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
    55  	shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
    56  
    57  	// anyHashPattern splits url containing SHA into parts
    58  	anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
    59  
    60  	// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
    61  	comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
    62  
    63  	// fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..."
    64  	fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`)
    65  
    66  	// emailRegex is definitely not perfect with edge cases,
    67  	// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
    68  	//   http://spec.commonmark.org/0.28/#email-address
    69  	//   https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
    70  	emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
    71  
    72  	// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
    73  	blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
    74  
    75  	// emojiShortCodeRegex find emoji by alias like :smile:
    76  	emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
    77  )
    78  
    79  // CSS class for action keywords (e.g. "closes: #1")
    80  const keywordClass = "issue-keyword"
    81  
    82  // IsFullURLBytes reports whether link fits valid format.
    83  func IsFullURLBytes(link []byte) bool {
    84  	return fullURLPattern.Match(link)
    85  }
    86  
    87  func IsFullURLString(link string) bool {
    88  	return fullURLPattern.MatchString(link)
    89  }
    90  
    91  func IsNonEmptyRelativePath(link string) bool {
    92  	return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#'
    93  }
    94  
    95  // regexp for full links to issues/pulls
    96  var issueFullPattern *regexp.Regexp
    97  
    98  // Once for to prevent races
    99  var issueFullPatternOnce sync.Once
   100  
   101  // regexp for full links to hash comment in pull request files changed tab
   102  var filesChangedFullPattern *regexp.Regexp
   103  
   104  // Once for to prevent races
   105  var filesChangedFullPatternOnce sync.Once
   106  
   107  func getIssueFullPattern() *regexp.Regexp {
   108  	issueFullPatternOnce.Do(func() {
   109  		// example: https://domain/org/repo/pulls/27#hash
   110  		issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
   111  			`[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
   112  	})
   113  	return issueFullPattern
   114  }
   115  
   116  func getFilesChangedFullPattern() *regexp.Regexp {
   117  	filesChangedFullPatternOnce.Do(func() {
   118  		// example: https://domain/org/repo/pulls/27/files#hash
   119  		filesChangedFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
   120  			`[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
   121  	})
   122  	return filesChangedFullPattern
   123  }
   124  
   125  // CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text
   126  func CustomLinkURLSchemes(schemes []string) {
   127  	schemes = append(schemes, "http", "https")
   128  	withAuth := make([]string, 0, len(schemes))
   129  	validScheme := regexp.MustCompile(`^[a-z]+$`)
   130  	for _, s := range schemes {
   131  		if !validScheme.MatchString(s) {
   132  			continue
   133  		}
   134  		without := false
   135  		for _, sna := range xurls.SchemesNoAuthority {
   136  			if s == sna {
   137  				without = true
   138  				break
   139  			}
   140  		}
   141  		if without {
   142  			s += ":"
   143  		} else {
   144  			s += "://"
   145  		}
   146  		withAuth = append(withAuth, s)
   147  	}
   148  	common.LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|"))
   149  }
   150  
   151  // IsSameDomain checks if given url string has the same hostname as current Gitea instance
   152  func IsSameDomain(s string) bool {
   153  	if strings.HasPrefix(s, "/") {
   154  		return true
   155  	}
   156  	if uapp, err := url.Parse(setting.AppURL); err == nil {
   157  		if u, err := url.Parse(s); err == nil {
   158  			return u.Host == uapp.Host
   159  		}
   160  		return false
   161  	}
   162  	return false
   163  }
   164  
   165  type postProcessError struct {
   166  	context string
   167  	err     error
   168  }
   169  
   170  func (p *postProcessError) Error() string {
   171  	return "PostProcess: " + p.context + ", " + p.err.Error()
   172  }
   173  
   174  type processor func(ctx *RenderContext, node *html.Node)
   175  
   176  var defaultProcessors = []processor{
   177  	fullIssuePatternProcessor,
   178  	comparePatternProcessor,
   179  	codePreviewPatternProcessor,
   180  	fullHashPatternProcessor,
   181  	shortLinkProcessor,
   182  	linkProcessor,
   183  	mentionProcessor,
   184  	issueIndexPatternProcessor,
   185  	commitCrossReferencePatternProcessor,
   186  	hashCurrentPatternProcessor,
   187  	emailAddressProcessor,
   188  	emojiProcessor,
   189  	emojiShortCodeProcessor,
   190  }
   191  
   192  // PostProcess does the final required transformations to the passed raw HTML
   193  // data, and ensures its validity. Transformations include: replacing links and
   194  // emails with HTML links, parsing shortlinks in the format of [[Link]], like
   195  // MediaWiki, linking issues in the format #ID, and mentions in the format
   196  // @user, and others.
   197  func PostProcess(
   198  	ctx *RenderContext,
   199  	input io.Reader,
   200  	output io.Writer,
   201  ) error {
   202  	return postProcess(ctx, defaultProcessors, input, output)
   203  }
   204  
   205  var commitMessageProcessors = []processor{
   206  	fullIssuePatternProcessor,
   207  	comparePatternProcessor,
   208  	fullHashPatternProcessor,
   209  	linkProcessor,
   210  	mentionProcessor,
   211  	issueIndexPatternProcessor,
   212  	commitCrossReferencePatternProcessor,
   213  	hashCurrentPatternProcessor,
   214  	emailAddressProcessor,
   215  	emojiProcessor,
   216  	emojiShortCodeProcessor,
   217  }
   218  
   219  // RenderCommitMessage will use the same logic as PostProcess, but will disable
   220  // the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
   221  // set, which changes every text node into a link to the passed default link.
   222  func RenderCommitMessage(
   223  	ctx *RenderContext,
   224  	content string,
   225  ) (string, error) {
   226  	procs := commitMessageProcessors
   227  	if ctx.DefaultLink != "" {
   228  		// we don't have to fear data races, because being
   229  		// commitMessageProcessors of fixed len and cap, every time we append
   230  		// something to it the slice is realloc+copied, so append always
   231  		// generates the slice ex-novo.
   232  		procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink))
   233  	}
   234  	return renderProcessString(ctx, procs, content)
   235  }
   236  
   237  var commitMessageSubjectProcessors = []processor{
   238  	fullIssuePatternProcessor,
   239  	comparePatternProcessor,
   240  	fullHashPatternProcessor,
   241  	linkProcessor,
   242  	mentionProcessor,
   243  	issueIndexPatternProcessor,
   244  	commitCrossReferencePatternProcessor,
   245  	hashCurrentPatternProcessor,
   246  	emojiShortCodeProcessor,
   247  	emojiProcessor,
   248  }
   249  
   250  var emojiProcessors = []processor{
   251  	emojiShortCodeProcessor,
   252  	emojiProcessor,
   253  }
   254  
   255  // RenderCommitMessageSubject will use the same logic as PostProcess and
   256  // RenderCommitMessage, but will disable the shortLinkProcessor and
   257  // emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
   258  // which changes every text node into a link to the passed default link.
   259  func RenderCommitMessageSubject(
   260  	ctx *RenderContext,
   261  	content string,
   262  ) (string, error) {
   263  	procs := commitMessageSubjectProcessors
   264  	if ctx.DefaultLink != "" {
   265  		// we don't have to fear data races, because being
   266  		// commitMessageSubjectProcessors of fixed len and cap, every time we
   267  		// append something to it the slice is realloc+copied, so append always
   268  		// generates the slice ex-novo.
   269  		procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink))
   270  	}
   271  	return renderProcessString(ctx, procs, content)
   272  }
   273  
   274  // RenderIssueTitle to process title on individual issue/pull page
   275  func RenderIssueTitle(
   276  	ctx *RenderContext,
   277  	title string,
   278  ) (string, error) {
   279  	return renderProcessString(ctx, []processor{
   280  		issueIndexPatternProcessor,
   281  		commitCrossReferencePatternProcessor,
   282  		hashCurrentPatternProcessor,
   283  		emojiShortCodeProcessor,
   284  		emojiProcessor,
   285  	}, title)
   286  }
   287  
   288  func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
   289  	var buf strings.Builder
   290  	if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
   291  		return "", err
   292  	}
   293  	return buf.String(), nil
   294  }
   295  
   296  // RenderDescriptionHTML will use similar logic as PostProcess, but will
   297  // use a single special linkProcessor.
   298  func RenderDescriptionHTML(
   299  	ctx *RenderContext,
   300  	content string,
   301  ) (string, error) {
   302  	return renderProcessString(ctx, []processor{
   303  		descriptionLinkProcessor,
   304  		emojiShortCodeProcessor,
   305  		emojiProcessor,
   306  	}, content)
   307  }
   308  
   309  // RenderEmoji for when we want to just process emoji and shortcodes
   310  // in various places it isn't already run through the normal markdown processor
   311  func RenderEmoji(
   312  	ctx *RenderContext,
   313  	content string,
   314  ) (string, error) {
   315  	return renderProcessString(ctx, emojiProcessors, content)
   316  }
   317  
   318  var (
   319  	tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`)
   320  	nulCleaner = strings.NewReplacer("\000", "")
   321  )
   322  
   323  func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
   324  	defer ctx.Cancel()
   325  	// FIXME: don't read all content to memory
   326  	rawHTML, err := io.ReadAll(input)
   327  	if err != nil {
   328  		return err
   329  	}
   330  
   331  	// parse the HTML
   332  	node, err := html.Parse(io.MultiReader(
   333  		// prepend "<html><body>"
   334  		strings.NewReader("<html><body>"),
   335  		// Strip out nuls - they're always invalid
   336  		bytes.NewReader(tagCleaner.ReplaceAll([]byte(nulCleaner.Replace(string(rawHTML))), []byte("&lt;$1"))),
   337  		// close the tags
   338  		strings.NewReader("</body></html>"),
   339  	))
   340  	if err != nil {
   341  		return &postProcessError{"invalid HTML", err}
   342  	}
   343  
   344  	if node.Type == html.DocumentNode {
   345  		node = node.FirstChild
   346  	}
   347  
   348  	visitNode(ctx, procs, node)
   349  
   350  	newNodes := make([]*html.Node, 0, 5)
   351  
   352  	if node.Data == "html" {
   353  		node = node.FirstChild
   354  		for node != nil && node.Data != "body" {
   355  			node = node.NextSibling
   356  		}
   357  	}
   358  	if node != nil {
   359  		if node.Data == "body" {
   360  			child := node.FirstChild
   361  			for child != nil {
   362  				newNodes = append(newNodes, child)
   363  				child = child.NextSibling
   364  			}
   365  		} else {
   366  			newNodes = append(newNodes, node)
   367  		}
   368  	}
   369  
   370  	// Render everything to buf.
   371  	for _, node := range newNodes {
   372  		if err := html.Render(output, node); err != nil {
   373  			return &postProcessError{"error rendering processed HTML", err}
   374  		}
   375  	}
   376  	return nil
   377  }
   378  
   379  func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
   380  	// Add user-content- to IDs and "#" links if they don't already have them
   381  	for idx, attr := range node.Attr {
   382  		val := strings.TrimPrefix(attr.Val, "#")
   383  		notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val))
   384  
   385  		if attr.Key == "id" && notHasPrefix {
   386  			node.Attr[idx].Val = "user-content-" + attr.Val
   387  		}
   388  
   389  		if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix {
   390  			node.Attr[idx].Val = "#user-content-" + val
   391  		}
   392  
   393  		if attr.Key == "class" && attr.Val == "emoji" {
   394  			procs = nil
   395  		}
   396  	}
   397  
   398  	switch node.Type {
   399  	case html.TextNode:
   400  		textNode(ctx, procs, node)
   401  	case html.ElementNode:
   402  		if node.Data == "code" || node.Data == "pre" {
   403  			// ignore code and pre nodes
   404  			return node.NextSibling
   405  		} else if node.Data == "img" {
   406  			return visitNodeImg(ctx, node)
   407  		} else if node.Data == "video" {
   408  			return visitNodeVideo(ctx, node)
   409  		} else if node.Data == "a" {
   410  			// Restrict text in links to emojis
   411  			procs = emojiProcessors
   412  		} else if node.Data == "i" {
   413  			for _, attr := range node.Attr {
   414  				if attr.Key != "class" {
   415  					continue
   416  				}
   417  				classes := strings.Split(attr.Val, " ")
   418  				for i, class := range classes {
   419  					if class == "icon" {
   420  						classes[0], classes[i] = classes[i], classes[0]
   421  						attr.Val = strings.Join(classes, " ")
   422  
   423  						// Remove all children of icons
   424  						child := node.FirstChild
   425  						for child != nil {
   426  							node.RemoveChild(child)
   427  							child = node.FirstChild
   428  						}
   429  						break
   430  					}
   431  				}
   432  			}
   433  		}
   434  		for n := node.FirstChild; n != nil; {
   435  			n = visitNode(ctx, procs, n)
   436  		}
   437  	}
   438  	return node.NextSibling
   439  }
   440  
   441  // textNode runs the passed node through various processors, in order to handle
   442  // all kinds of special links handled by the post-processing.
   443  func textNode(ctx *RenderContext, procs []processor, node *html.Node) {
   444  	for _, processor := range procs {
   445  		processor(ctx, node)
   446  	}
   447  }
   448  
   449  // createKeyword() renders a highlighted version of an action keyword
   450  func createKeyword(content string) *html.Node {
   451  	span := &html.Node{
   452  		Type: html.ElementNode,
   453  		Data: atom.Span.String(),
   454  		Attr: []html.Attribute{},
   455  	}
   456  	span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass})
   457  
   458  	text := &html.Node{
   459  		Type: html.TextNode,
   460  		Data: content,
   461  	}
   462  	span.AppendChild(text)
   463  
   464  	return span
   465  }
   466  
   467  func createEmoji(content, class, name string) *html.Node {
   468  	span := &html.Node{
   469  		Type: html.ElementNode,
   470  		Data: atom.Span.String(),
   471  		Attr: []html.Attribute{},
   472  	}
   473  	if class != "" {
   474  		span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: class})
   475  	}
   476  	if name != "" {
   477  		span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: name})
   478  	}
   479  
   480  	text := &html.Node{
   481  		Type: html.TextNode,
   482  		Data: content,
   483  	}
   484  
   485  	span.AppendChild(text)
   486  	return span
   487  }
   488  
   489  func createCustomEmoji(alias string) *html.Node {
   490  	span := &html.Node{
   491  		Type: html.ElementNode,
   492  		Data: atom.Span.String(),
   493  		Attr: []html.Attribute{},
   494  	}
   495  	span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: "emoji"})
   496  	span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
   497  
   498  	img := &html.Node{
   499  		Type:     html.ElementNode,
   500  		DataAtom: atom.Img,
   501  		Data:     "img",
   502  		Attr:     []html.Attribute{},
   503  	}
   504  	img.Attr = append(img.Attr, html.Attribute{Key: "alt", Val: ":" + alias + ":"})
   505  	img.Attr = append(img.Attr, html.Attribute{Key: "src", Val: setting.StaticURLPrefix + "/assets/img/emoji/" + alias + ".png"})
   506  
   507  	span.AppendChild(img)
   508  	return span
   509  }
   510  
   511  func createLink(href, content, class string) *html.Node {
   512  	a := &html.Node{
   513  		Type: html.ElementNode,
   514  		Data: atom.A.String(),
   515  		Attr: []html.Attribute{{Key: "href", Val: href}},
   516  	}
   517  
   518  	if class != "" {
   519  		a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
   520  	}
   521  
   522  	text := &html.Node{
   523  		Type: html.TextNode,
   524  		Data: content,
   525  	}
   526  
   527  	a.AppendChild(text)
   528  	return a
   529  }
   530  
   531  func createCodeLink(href, content, class string) *html.Node {
   532  	a := &html.Node{
   533  		Type: html.ElementNode,
   534  		Data: atom.A.String(),
   535  		Attr: []html.Attribute{{Key: "href", Val: href}},
   536  	}
   537  
   538  	if class != "" {
   539  		a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
   540  	}
   541  
   542  	text := &html.Node{
   543  		Type: html.TextNode,
   544  		Data: content,
   545  	}
   546  
   547  	code := &html.Node{
   548  		Type: html.ElementNode,
   549  		Data: atom.Code.String(),
   550  		Attr: []html.Attribute{{Key: "class", Val: "nohighlight"}},
   551  	}
   552  
   553  	code.AppendChild(text)
   554  	a.AppendChild(code)
   555  	return a
   556  }
   557  
   558  // replaceContent takes text node, and in its content it replaces a section of
   559  // it with the specified newNode.
   560  func replaceContent(node *html.Node, i, j int, newNode *html.Node) {
   561  	replaceContentList(node, i, j, []*html.Node{newNode})
   562  }
   563  
   564  // replaceContentList takes text node, and in its content it replaces a section of
   565  // it with the specified newNodes. An example to visualize how this can work can
   566  // be found here: https://play.golang.org/p/5zP8NnHZ03s
   567  func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
   568  	// get the data before and after the match
   569  	before := node.Data[:i]
   570  	after := node.Data[j:]
   571  
   572  	// Replace in the current node the text, so that it is only what it is
   573  	// supposed to have.
   574  	node.Data = before
   575  
   576  	// Get the current next sibling, before which we place the replaced data,
   577  	// and after that we place the new text node.
   578  	nextSibling := node.NextSibling
   579  	for _, n := range newNodes {
   580  		node.Parent.InsertBefore(n, nextSibling)
   581  	}
   582  	if after != "" {
   583  		node.Parent.InsertBefore(&html.Node{
   584  			Type: html.TextNode,
   585  			Data: after,
   586  		}, nextSibling)
   587  	}
   588  }
   589  
   590  func mentionProcessor(ctx *RenderContext, node *html.Node) {
   591  	start := 0
   592  	nodeStop := node.NextSibling
   593  	for node != nodeStop {
   594  		found, loc := references.FindFirstMentionBytes(util.UnsafeStringToBytes(node.Data[start:]))
   595  		if !found {
   596  			node = node.NextSibling
   597  			start = 0
   598  			continue
   599  		}
   600  		loc.Start += start
   601  		loc.End += start
   602  		mention := node.Data[loc.Start:loc.End]
   603  		teams, ok := ctx.Metas["teams"]
   604  		// FIXME: util.URLJoin may not be necessary here:
   605  		// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
   606  		// is an AppSubURL link we can probably fallback to concatenation.
   607  		// team mention should follow @orgName/teamName style
   608  		if ok && strings.Contains(mention, "/") {
   609  			mentionOrgAndTeam := strings.Split(mention, "/")
   610  			if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
   611  				replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
   612  				node = node.NextSibling.NextSibling
   613  				start = 0
   614  				continue
   615  			}
   616  			start = loc.End
   617  			continue
   618  		}
   619  		mentionedUsername := mention[1:]
   620  
   621  		if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
   622  			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention"))
   623  			node = node.NextSibling.NextSibling
   624  			start = 0
   625  		} else {
   626  			start = loc.End
   627  		}
   628  	}
   629  }
   630  
   631  func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
   632  	next := node.NextSibling
   633  	for node != nil && node != next {
   634  		m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
   635  		if m == nil {
   636  			return
   637  		}
   638  
   639  		content := node.Data[m[2]:m[3]]
   640  		tail := node.Data[m[4]:m[5]]
   641  		props := make(map[string]string)
   642  
   643  		// MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
   644  		// It makes page handling terrible, but we prefer GitHub syntax
   645  		// And fall back to MediaWiki only when it is obvious from the look
   646  		// Of text and link contents
   647  		sl := strings.Split(content, "|")
   648  		for _, v := range sl {
   649  			if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
   650  				// There is no equal in this argument; this is a mandatory arg
   651  				if props["name"] == "" {
   652  					if IsFullURLString(v) {
   653  						// If we clearly see it is a link, we save it so
   654  
   655  						// But first we need to ensure, that if both mandatory args provided
   656  						// look like links, we stick to GitHub syntax
   657  						if props["link"] != "" {
   658  							props["name"] = props["link"]
   659  						}
   660  
   661  						props["link"] = strings.TrimSpace(v)
   662  					} else {
   663  						props["name"] = v
   664  					}
   665  				} else {
   666  					props["link"] = strings.TrimSpace(v)
   667  				}
   668  			} else {
   669  				// There is an equal; optional argument.
   670  
   671  				sep := strings.IndexByte(v, '=')
   672  				key, val := v[:sep], html.UnescapeString(v[sep+1:])
   673  
   674  				// When parsing HTML, x/net/html will change all quotes which are
   675  				// not used for syntax into UTF-8 quotes. So checking val[0] won't
   676  				// be enough, since that only checks a single byte.
   677  				if len(val) > 1 {
   678  					if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) ||
   679  						(strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) {
   680  						const lenQuote = len("‘")
   681  						val = val[lenQuote : len(val)-lenQuote]
   682  					} else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) ||
   683  						(strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) {
   684  						val = val[1 : len(val)-1]
   685  					} else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") {
   686  						const lenQuote = len("‘")
   687  						val = val[1 : len(val)-lenQuote]
   688  					}
   689  				}
   690  				props[key] = val
   691  			}
   692  		}
   693  
   694  		var name, link string
   695  		if props["link"] != "" {
   696  			link = props["link"]
   697  		} else if props["name"] != "" {
   698  			link = props["name"]
   699  		}
   700  		if props["title"] != "" {
   701  			name = props["title"]
   702  		} else if props["name"] != "" {
   703  			name = props["name"]
   704  		} else {
   705  			name = link
   706  		}
   707  
   708  		name += tail
   709  		image := false
   710  		ext := filepath.Ext(link)
   711  		switch ext {
   712  		// fast path: empty string, ignore
   713  		case "":
   714  			// leave image as false
   715  		case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
   716  			image = true
   717  		}
   718  
   719  		childNode := &html.Node{}
   720  		linkNode := &html.Node{
   721  			FirstChild: childNode,
   722  			LastChild:  childNode,
   723  			Type:       html.ElementNode,
   724  			Data:       "a",
   725  			DataAtom:   atom.A,
   726  		}
   727  		childNode.Parent = linkNode
   728  		absoluteLink := IsFullURLString(link)
   729  		if !absoluteLink {
   730  			if image {
   731  				link = strings.ReplaceAll(link, " ", "+")
   732  			} else {
   733  				link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-"
   734  			}
   735  			if !strings.Contains(link, "/") {
   736  				link = url.PathEscape(link) // FIXME: it doesn't seem right and it might cause double-escaping
   737  			}
   738  		}
   739  		if image {
   740  			if !absoluteLink {
   741  				link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), link)
   742  			}
   743  			title := props["title"]
   744  			if title == "" {
   745  				title = props["alt"]
   746  			}
   747  			if title == "" {
   748  				title = path.Base(name)
   749  			}
   750  			alt := props["alt"]
   751  			if alt == "" {
   752  				alt = name
   753  			}
   754  
   755  			// make the childNode an image - if we can, we also place the alt
   756  			childNode.Type = html.ElementNode
   757  			childNode.Data = "img"
   758  			childNode.DataAtom = atom.Img
   759  			childNode.Attr = []html.Attribute{
   760  				{Key: "src", Val: link},
   761  				{Key: "title", Val: title},
   762  				{Key: "alt", Val: alt},
   763  			}
   764  			if alt == "" {
   765  				childNode.Attr = childNode.Attr[:2]
   766  			}
   767  		} else {
   768  			link, _ = ResolveLink(ctx, link, "")
   769  			childNode.Type = html.TextNode
   770  			childNode.Data = name
   771  		}
   772  		linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
   773  		replaceContent(node, m[0], m[1], linkNode)
   774  		node = node.NextSibling.NextSibling
   775  	}
   776  }
   777  
   778  func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
   779  	if ctx.Metas == nil {
   780  		return
   781  	}
   782  	next := node.NextSibling
   783  	for node != nil && node != next {
   784  		m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
   785  		if m == nil {
   786  			return
   787  		}
   788  
   789  		mDiffView := getFilesChangedFullPattern().FindStringSubmatchIndex(node.Data)
   790  		// leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files
   791  		if mDiffView != nil {
   792  			return
   793  		}
   794  
   795  		link := node.Data[m[0]:m[1]]
   796  		text := "#" + node.Data[m[2]:m[3]]
   797  		// if m[4] and m[5] is not -1, then link is to a comment
   798  		// indicate that in the text by appending (comment)
   799  		if m[4] != -1 && m[5] != -1 {
   800  			if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
   801  				text += " " + locale.TrString("repo.from_comment")
   802  			} else {
   803  				text += " (comment)"
   804  			}
   805  		}
   806  
   807  		// extract repo and org name from matched link like
   808  		// http://localhost:3000/gituser/myrepo/issues/1
   809  		linkParts := strings.Split(link, "/")
   810  		matchOrg := linkParts[len(linkParts)-4]
   811  		matchRepo := linkParts[len(linkParts)-3]
   812  
   813  		if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] {
   814  			replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
   815  		} else {
   816  			text = matchOrg + "/" + matchRepo + text
   817  			replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
   818  		}
   819  		node = node.NextSibling.NextSibling
   820  	}
   821  }
   822  
   823  func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
   824  	if ctx.Metas == nil {
   825  		return
   826  	}
   827  
   828  	// FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
   829  	// The "mode" approach should be refactored to some other more clear&reliable way.
   830  	crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
   831  
   832  	var (
   833  		found bool
   834  		ref   *references.RenderizableReference
   835  	)
   836  
   837  	next := node.NextSibling
   838  
   839  	for node != nil && node != next {
   840  		_, hasExtTrackFormat := ctx.Metas["format"]
   841  
   842  		// Repos with external issue trackers might still need to reference local PRs
   843  		// We need to concern with the first one that shows up in the text, whichever it is
   844  		isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
   845  		foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
   846  
   847  		switch ctx.Metas["style"] {
   848  		case "", IssueNameStyleNumeric:
   849  			found, ref = foundNumeric, refNumeric
   850  		case IssueNameStyleAlphanumeric:
   851  			found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
   852  		case IssueNameStyleRegexp:
   853  			pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"])
   854  			if err != nil {
   855  				return
   856  			}
   857  			found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
   858  		}
   859  
   860  		// Repos with external issue trackers might still need to reference local PRs
   861  		// We need to concern with the first one that shows up in the text, whichever it is
   862  		if hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
   863  			// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
   864  			// Allow a free-pass when non-numeric pattern wasn't found.
   865  			if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) {
   866  				found = foundNumeric
   867  				ref = refNumeric
   868  			}
   869  		}
   870  		if !found {
   871  			return
   872  		}
   873  
   874  		var link *html.Node
   875  		reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
   876  		if hasExtTrackFormat && !ref.IsPull {
   877  			ctx.Metas["index"] = ref.Issue
   878  
   879  			res, err := vars.Expand(ctx.Metas["format"], ctx.Metas)
   880  			if err != nil {
   881  				// here we could just log the error and continue the rendering
   882  				log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
   883  			}
   884  
   885  			link = createLink(res, reftext, "ref-issue ref-external-issue")
   886  		} else {
   887  			// Path determines the type of link that will be rendered. It's unknown at this point whether
   888  			// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
   889  			// Gitea will redirect on click as appropriate.
   890  			path := "issues"
   891  			if ref.IsPull {
   892  				path = "pulls"
   893  			}
   894  			if ref.Owner == "" {
   895  				link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue")
   896  			} else {
   897  				link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
   898  			}
   899  		}
   900  
   901  		if ref.Action == references.XRefActionNone {
   902  			replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
   903  			node = node.NextSibling.NextSibling
   904  			continue
   905  		}
   906  
   907  		// Decorate action keywords if actionable
   908  		var keyword *html.Node
   909  		if references.IsXrefActionable(ref, hasExtTrackFormat) {
   910  			keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
   911  		} else {
   912  			keyword = &html.Node{
   913  				Type: html.TextNode,
   914  				Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End],
   915  			}
   916  		}
   917  		spaces := &html.Node{
   918  			Type: html.TextNode,
   919  			Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
   920  		}
   921  		replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link})
   922  		node = node.NextSibling.NextSibling.NextSibling.NextSibling
   923  	}
   924  }
   925  
   926  func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
   927  	next := node.NextSibling
   928  
   929  	for node != nil && node != next {
   930  		found, ref := references.FindRenderizableCommitCrossReference(node.Data)
   931  		if !found {
   932  			return
   933  		}
   934  
   935  		reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
   936  		link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
   937  
   938  		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
   939  		node = node.NextSibling.NextSibling
   940  	}
   941  }
   942  
   943  type anyHashPatternResult struct {
   944  	PosStart  int
   945  	PosEnd    int
   946  	FullURL   string
   947  	CommitID  string
   948  	SubPath   string
   949  	QueryHash string
   950  }
   951  
   952  func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
   953  	m := anyHashPattern.FindStringSubmatchIndex(s)
   954  	if m == nil {
   955  		return ret, false
   956  	}
   957  
   958  	ret.PosStart, ret.PosEnd = m[0], m[1]
   959  	ret.FullURL = s[ret.PosStart:ret.PosEnd]
   960  	if strings.HasSuffix(ret.FullURL, ".") {
   961  		// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
   962  		ret.PosEnd--
   963  		ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
   964  		for i := 0; i < len(m); i++ {
   965  			m[i] = min(m[i], ret.PosEnd)
   966  		}
   967  	}
   968  
   969  	ret.CommitID = s[m[2]:m[3]]
   970  	if m[5] > 0 {
   971  		ret.SubPath = s[m[4]:m[5]]
   972  	}
   973  
   974  	lastStart, lastEnd := m[len(m)-2], m[len(m)-1]
   975  	if lastEnd > 0 {
   976  		ret.QueryHash = s[lastStart:lastEnd][1:]
   977  	}
   978  	return ret, true
   979  }
   980  
   981  // fullHashPatternProcessor renders SHA containing URLs
   982  func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
   983  	if ctx.Metas == nil {
   984  		return
   985  	}
   986  	nodeStop := node.NextSibling
   987  	for node != nodeStop {
   988  		if node.Type != html.TextNode {
   989  			node = node.NextSibling
   990  			continue
   991  		}
   992  		ret, ok := anyHashPatternExtract(node.Data)
   993  		if !ok {
   994  			node = node.NextSibling
   995  			continue
   996  		}
   997  		text := base.ShortSha(ret.CommitID)
   998  		if ret.SubPath != "" {
   999  			text += ret.SubPath
  1000  		}
  1001  		if ret.QueryHash != "" {
  1002  			text += " (" + ret.QueryHash + ")"
  1003  		}
  1004  		replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
  1005  		node = node.NextSibling.NextSibling
  1006  	}
  1007  }
  1008  
  1009  func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
  1010  	if ctx.Metas == nil {
  1011  		return
  1012  	}
  1013  	nodeStop := node.NextSibling
  1014  	for node != nodeStop {
  1015  		if node.Type != html.TextNode {
  1016  			node = node.NextSibling
  1017  			continue
  1018  		}
  1019  		m := comparePattern.FindStringSubmatchIndex(node.Data)
  1020  		if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
  1021  			node = node.NextSibling
  1022  			continue
  1023  		}
  1024  
  1025  		urlFull := node.Data[m[0]:m[1]]
  1026  		text1 := base.ShortSha(node.Data[m[2]:m[3]])
  1027  		textDots := base.ShortSha(node.Data[m[4]:m[5]])
  1028  		text2 := base.ShortSha(node.Data[m[6]:m[7]])
  1029  
  1030  		hash := ""
  1031  		if m[9] > 0 {
  1032  			hash = node.Data[m[8]:m[9]][1:]
  1033  		}
  1034  
  1035  		start := m[0]
  1036  		end := m[1]
  1037  
  1038  		// If url ends in '.', it's very likely that it is not part of the
  1039  		// actual url but used to finish a sentence.
  1040  		if strings.HasSuffix(urlFull, ".") {
  1041  			end--
  1042  			urlFull = urlFull[:len(urlFull)-1]
  1043  			if hash != "" {
  1044  				hash = hash[:len(hash)-1]
  1045  			} else if text2 != "" {
  1046  				text2 = text2[:len(text2)-1]
  1047  			}
  1048  		}
  1049  
  1050  		text := text1 + textDots + text2
  1051  		if hash != "" {
  1052  			text += " (" + hash + ")"
  1053  		}
  1054  		replaceContent(node, start, end, createCodeLink(urlFull, text, "compare"))
  1055  		node = node.NextSibling.NextSibling
  1056  	}
  1057  }
  1058  
  1059  // emojiShortCodeProcessor for rendering text like :smile: into emoji
  1060  func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
  1061  	start := 0
  1062  	next := node.NextSibling
  1063  	for node != nil && node != next && start < len(node.Data) {
  1064  		m := emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
  1065  		if m == nil {
  1066  			return
  1067  		}
  1068  		m[0] += start
  1069  		m[1] += start
  1070  
  1071  		start = m[1]
  1072  
  1073  		alias := node.Data[m[0]:m[1]]
  1074  		alias = strings.ReplaceAll(alias, ":", "")
  1075  		converted := emoji.FromAlias(alias)
  1076  		if converted == nil {
  1077  			// check if this is a custom reaction
  1078  			if _, exist := setting.UI.CustomEmojisMap[alias]; exist {
  1079  				replaceContent(node, m[0], m[1], createCustomEmoji(alias))
  1080  				node = node.NextSibling.NextSibling
  1081  				start = 0
  1082  				continue
  1083  			}
  1084  			continue
  1085  		}
  1086  
  1087  		replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description))
  1088  		node = node.NextSibling.NextSibling
  1089  		start = 0
  1090  	}
  1091  }
  1092  
  1093  // emoji processor to match emoji and add emoji class
  1094  func emojiProcessor(ctx *RenderContext, node *html.Node) {
  1095  	start := 0
  1096  	next := node.NextSibling
  1097  	for node != nil && node != next && start < len(node.Data) {
  1098  		m := emoji.FindEmojiSubmatchIndex(node.Data[start:])
  1099  		if m == nil {
  1100  			return
  1101  		}
  1102  		m[0] += start
  1103  		m[1] += start
  1104  
  1105  		codepoint := node.Data[m[0]:m[1]]
  1106  		start = m[1]
  1107  		val := emoji.FromCode(codepoint)
  1108  		if val != nil {
  1109  			replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description))
  1110  			node = node.NextSibling.NextSibling
  1111  			start = 0
  1112  		}
  1113  	}
  1114  }
  1115  
  1116  // hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
  1117  // are assumed to be in the same repository.
  1118  func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
  1119  	if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" {
  1120  		return
  1121  	}
  1122  
  1123  	start := 0
  1124  	next := node.NextSibling
  1125  	if ctx.ShaExistCache == nil {
  1126  		ctx.ShaExistCache = make(map[string]bool)
  1127  	}
  1128  	for node != nil && node != next && start < len(node.Data) {
  1129  		m := hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
  1130  		if m == nil {
  1131  			return
  1132  		}
  1133  		m[2] += start
  1134  		m[3] += start
  1135  
  1136  		hash := node.Data[m[2]:m[3]]
  1137  		// The regex does not lie, it matches the hash pattern.
  1138  		// However, a regex cannot know if a hash actually exists or not.
  1139  		// We could assume that a SHA1 hash should probably contain alphas AND numerics
  1140  		// but that is not always the case.
  1141  		// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
  1142  		// as used by git and github for linking and thus we have to do similar.
  1143  		// Because of this, we check to make sure that a matched hash is actually
  1144  		// a commit in the repository before making it a link.
  1145  
  1146  		// check cache first
  1147  		exist, inCache := ctx.ShaExistCache[hash]
  1148  		if !inCache {
  1149  			if ctx.GitRepo == nil {
  1150  				var err error
  1151  				ctx.GitRepo, err = git.OpenRepository(ctx.Ctx, ctx.Metas["repoPath"])
  1152  				if err != nil {
  1153  					log.Error("unable to open repository: %s Error: %v", ctx.Metas["repoPath"], err)
  1154  					return
  1155  				}
  1156  				ctx.AddCancel(func() {
  1157  					ctx.GitRepo.Close()
  1158  					ctx.GitRepo = nil
  1159  				})
  1160  			}
  1161  
  1162  			// Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
  1163  			exist = ctx.GitRepo.IsReferenceExist(hash)
  1164  			ctx.ShaExistCache[hash] = exist
  1165  		}
  1166  
  1167  		if !exist {
  1168  			start = m[3]
  1169  			continue
  1170  		}
  1171  
  1172  		link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
  1173  		replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
  1174  		start = 0
  1175  		node = node.NextSibling.NextSibling
  1176  	}
  1177  }
  1178  
  1179  // emailAddressProcessor replaces raw email addresses with a mailto: link.
  1180  func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
  1181  	next := node.NextSibling
  1182  	for node != nil && node != next {
  1183  		m := emailRegex.FindStringSubmatchIndex(node.Data)
  1184  		if m == nil {
  1185  			return
  1186  		}
  1187  
  1188  		mail := node.Data[m[2]:m[3]]
  1189  		replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto"))
  1190  		node = node.NextSibling.NextSibling
  1191  	}
  1192  }
  1193  
  1194  // linkProcessor creates links for any HTTP or HTTPS URL not captured by
  1195  // markdown.
  1196  func linkProcessor(ctx *RenderContext, node *html.Node) {
  1197  	next := node.NextSibling
  1198  	for node != nil && node != next {
  1199  		m := common.LinkRegex.FindStringIndex(node.Data)
  1200  		if m == nil {
  1201  			return
  1202  		}
  1203  
  1204  		uri := node.Data[m[0]:m[1]]
  1205  		replaceContent(node, m[0], m[1], createLink(uri, uri, "link"))
  1206  		node = node.NextSibling.NextSibling
  1207  	}
  1208  }
  1209  
  1210  func genDefaultLinkProcessor(defaultLink string) processor {
  1211  	return func(ctx *RenderContext, node *html.Node) {
  1212  		ch := &html.Node{
  1213  			Parent: node,
  1214  			Type:   html.TextNode,
  1215  			Data:   node.Data,
  1216  		}
  1217  
  1218  		node.Type = html.ElementNode
  1219  		node.Data = "a"
  1220  		node.DataAtom = atom.A
  1221  		node.Attr = []html.Attribute{
  1222  			{Key: "href", Val: defaultLink},
  1223  			{Key: "class", Val: "default-link muted"},
  1224  		}
  1225  		node.FirstChild, node.LastChild = ch, ch
  1226  	}
  1227  }
  1228  
  1229  // descriptionLinkProcessor creates links for DescriptionHTML
  1230  func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) {
  1231  	next := node.NextSibling
  1232  	for node != nil && node != next {
  1233  		m := common.LinkRegex.FindStringIndex(node.Data)
  1234  		if m == nil {
  1235  			return
  1236  		}
  1237  
  1238  		uri := node.Data[m[0]:m[1]]
  1239  		replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri))
  1240  		node = node.NextSibling.NextSibling
  1241  	}
  1242  }
  1243  
  1244  func createDescriptionLink(href, content string) *html.Node {
  1245  	textNode := &html.Node{
  1246  		Type: html.TextNode,
  1247  		Data: content,
  1248  	}
  1249  	linkNode := &html.Node{
  1250  		FirstChild: textNode,
  1251  		LastChild:  textNode,
  1252  		Type:       html.ElementNode,
  1253  		Data:       "a",
  1254  		DataAtom:   atom.A,
  1255  		Attr: []html.Attribute{
  1256  			{Key: "href", Val: href},
  1257  			{Key: "target", Val: "_blank"},
  1258  			{Key: "rel", Val: "noopener noreferrer"},
  1259  		},
  1260  	}
  1261  	textNode.Parent = linkNode
  1262  	return linkNode
  1263  }