Run processors on whole of text (#16155)
There is an inefficiency in the design of our processors which means that Emoji and other processors run in order n^2 time. This PR forces the processors to process the entirety of text node before passing back up. The fundamental inefficiency remains but it should be significantly ameliorated. Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
		
							parent
							
								
									6ad5d0a306
								
							
						
					
					
						commit
						0db1048c3a
					
				
					 3 changed files with 414 additions and 316 deletions
				
			
		|  | @ -6,6 +6,7 @@ | ||||||
| package emoji | package emoji | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"io" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | @ -145,6 +146,8 @@ func (n *rememberSecondWriteWriter) Write(p []byte) (int, error) { | ||||||
| 	if n.writecount == 2 { | 	if n.writecount == 2 { | ||||||
| 		n.idx = n.pos | 		n.idx = n.pos | ||||||
| 		n.end = n.pos + len(p) | 		n.end = n.pos + len(p) | ||||||
|  | 		n.pos += len(p) | ||||||
|  | 		return len(p), io.EOF | ||||||
| 	} | 	} | ||||||
| 	n.pos += len(p) | 	n.pos += len(p) | ||||||
| 	return len(p), nil | 	return len(p), nil | ||||||
|  | @ -155,6 +158,8 @@ func (n *rememberSecondWriteWriter) WriteString(s string) (int, error) { | ||||||
| 	if n.writecount == 2 { | 	if n.writecount == 2 { | ||||||
| 		n.idx = n.pos | 		n.idx = n.pos | ||||||
| 		n.end = n.pos + len(s) | 		n.end = n.pos + len(s) | ||||||
|  | 		n.pos += len(s) | ||||||
|  | 		return len(s), io.EOF | ||||||
| 	} | 	} | ||||||
| 	n.pos += len(s) | 	n.pos += len(s) | ||||||
| 	return len(s), nil | 	return len(s), nil | ||||||
|  |  | ||||||
|  | @ -89,6 +89,7 @@ func isLinkStr(link string) bool { | ||||||
| 	return validLinksPattern.MatchString(link) | 	return validLinksPattern.MatchString(link) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // FIXME: This function is not concurrent safe
 | ||||||
| func getIssueFullPattern() *regexp.Regexp { | func getIssueFullPattern() *regexp.Regexp { | ||||||
| 	if issueFullPattern == nil { | 	if issueFullPattern == nil { | ||||||
| 		issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) + | 		issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) + | ||||||
|  | @ -566,26 +567,38 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func mentionProcessor(ctx *RenderContext, node *html.Node) { | func mentionProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	// We replace only the first mention; other mentions will be addressed later
 | 	start := 0 | ||||||
| 	found, loc := references.FindFirstMentionBytes([]byte(node.Data)) | 	next := node.NextSibling | ||||||
| 	if !found { | 	for node != nil && node != next && start < len(node.Data) { | ||||||
| 		return | 		// We replace only the first mention; other mentions will be addressed later
 | ||||||
| 	} | 		found, loc := references.FindFirstMentionBytes([]byte(node.Data[start:])) | ||||||
| 	mention := node.Data[loc.Start:loc.End] | 		if !found { | ||||||
| 	var teams string | 			return | ||||||
| 	teams, ok := ctx.Metas["teams"] |  | ||||||
| 	// FIXME: util.URLJoin may not be necessary here:
 |  | ||||||
| 	// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
 |  | ||||||
| 	// is an AppSubURL link we can probably fallback to concatenation.
 |  | ||||||
| 	// team mention should follow @orgName/teamName style
 |  | ||||||
| 	if ok && strings.Contains(mention, "/") { |  | ||||||
| 		mentionOrgAndTeam := strings.Split(mention, "/") |  | ||||||
| 		if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { |  | ||||||
| 			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) |  | ||||||
| 		} | 		} | ||||||
| 		return | 		loc.Start += start | ||||||
|  | 		loc.End += start | ||||||
|  | 		mention := node.Data[loc.Start:loc.End] | ||||||
|  | 		var teams string | ||||||
|  | 		teams, ok := ctx.Metas["teams"] | ||||||
|  | 		// FIXME: util.URLJoin may not be necessary here:
 | ||||||
|  | 		// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
 | ||||||
|  | 		// is an AppSubURL link we can probably fallback to concatenation.
 | ||||||
|  | 		// team mention should follow @orgName/teamName style
 | ||||||
|  | 		if ok && strings.Contains(mention, "/") { | ||||||
|  | 			mentionOrgAndTeam := strings.Split(mention, "/") | ||||||
|  | 			if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { | ||||||
|  | 				replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) | ||||||
|  | 				node = node.NextSibling.NextSibling | ||||||
|  | 				start = 0 | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			start = loc.End | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
|  | 		start = 0 | ||||||
| 	} | 	} | ||||||
| 	replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
|  | @ -593,188 +606,196 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) { | func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) { | ||||||
| 	m := shortLinkPattern.FindStringSubmatchIndex(node.Data) | 	next := node.NextSibling | ||||||
| 	if m == nil { | 	for node != nil && node != next { | ||||||
| 		return | 		m := shortLinkPattern.FindStringSubmatchIndex(node.Data) | ||||||
| 	} | 		if m == nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 	content := node.Data[m[2]:m[3]] | 		content := node.Data[m[2]:m[3]] | ||||||
| 	tail := node.Data[m[4]:m[5]] | 		tail := node.Data[m[4]:m[5]] | ||||||
| 	props := make(map[string]string) | 		props := make(map[string]string) | ||||||
| 
 | 
 | ||||||
| 	// MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
 | 		// MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
 | ||||||
| 	// It makes page handling terrible, but we prefer GitHub syntax
 | 		// It makes page handling terrible, but we prefer GitHub syntax
 | ||||||
| 	// And fall back to MediaWiki only when it is obvious from the look
 | 		// And fall back to MediaWiki only when it is obvious from the look
 | ||||||
| 	// Of text and link contents
 | 		// Of text and link contents
 | ||||||
| 	sl := strings.Split(content, "|") | 		sl := strings.Split(content, "|") | ||||||
| 	for _, v := range sl { | 		for _, v := range sl { | ||||||
| 		if equalPos := strings.IndexByte(v, '='); equalPos == -1 { | 			if equalPos := strings.IndexByte(v, '='); equalPos == -1 { | ||||||
| 			// There is no equal in this argument; this is a mandatory arg
 | 				// There is no equal in this argument; this is a mandatory arg
 | ||||||
| 			if props["name"] == "" { | 				if props["name"] == "" { | ||||||
| 				if isLinkStr(v) { | 					if isLinkStr(v) { | ||||||
| 					// If we clearly see it is a link, we save it so
 | 						// If we clearly see it is a link, we save it so
 | ||||||
| 
 | 
 | ||||||
| 					// But first we need to ensure, that if both mandatory args provided
 | 						// But first we need to ensure, that if both mandatory args provided
 | ||||||
| 					// look like links, we stick to GitHub syntax
 | 						// look like links, we stick to GitHub syntax
 | ||||||
| 					if props["link"] != "" { | 						if props["link"] != "" { | ||||||
| 						props["name"] = props["link"] | 							props["name"] = props["link"] | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						props["link"] = strings.TrimSpace(v) | ||||||
|  | 					} else { | ||||||
|  | 						props["name"] = v | ||||||
| 					} | 					} | ||||||
| 
 |  | ||||||
| 					props["link"] = strings.TrimSpace(v) |  | ||||||
| 				} else { | 				} else { | ||||||
| 					props["name"] = v | 					props["link"] = strings.TrimSpace(v) | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				props["link"] = strings.TrimSpace(v) | 				// There is an equal; optional argument.
 | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			// There is an equal; optional argument.
 |  | ||||||
| 
 | 
 | ||||||
| 			sep := strings.IndexByte(v, '=') | 				sep := strings.IndexByte(v, '=') | ||||||
| 			key, val := v[:sep], html.UnescapeString(v[sep+1:]) | 				key, val := v[:sep], html.UnescapeString(v[sep+1:]) | ||||||
| 
 | 
 | ||||||
| 			// When parsing HTML, x/net/html will change all quotes which are
 | 				// When parsing HTML, x/net/html will change all quotes which are
 | ||||||
| 			// not used for syntax into UTF-8 quotes. So checking val[0] won't
 | 				// not used for syntax into UTF-8 quotes. So checking val[0] won't
 | ||||||
| 			// be enough, since that only checks a single byte.
 | 				// be enough, since that only checks a single byte.
 | ||||||
| 			if len(val) > 1 { | 				if len(val) > 1 { | ||||||
| 				if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) || | 					if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) || | ||||||
| 					(strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) { | 						(strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) { | ||||||
| 					const lenQuote = len("‘") | 						const lenQuote = len("‘") | ||||||
| 					val = val[lenQuote : len(val)-lenQuote] | 						val = val[lenQuote : len(val)-lenQuote] | ||||||
| 				} else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) || | 					} else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) || | ||||||
| 					(strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) { | 						(strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) { | ||||||
| 					val = val[1 : len(val)-1] | 						val = val[1 : len(val)-1] | ||||||
| 				} else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") { | 					} else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") { | ||||||
| 					const lenQuote = len("‘") | 						const lenQuote = len("‘") | ||||||
| 					val = val[1 : len(val)-lenQuote] | 						val = val[1 : len(val)-lenQuote] | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
|  | 				props[key] = val | ||||||
| 			} | 			} | ||||||
| 			props[key] = val |  | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	var name, link string | 		var name, link string | ||||||
| 	if props["link"] != "" { | 		if props["link"] != "" { | ||||||
| 		link = props["link"] | 			link = props["link"] | ||||||
| 	} else if props["name"] != "" { | 		} else if props["name"] != "" { | ||||||
| 		link = props["name"] | 			link = props["name"] | ||||||
| 	} | 		} | ||||||
| 	if props["title"] != "" { | 		if props["title"] != "" { | ||||||
| 		name = props["title"] | 			name = props["title"] | ||||||
| 	} else if props["name"] != "" { | 		} else if props["name"] != "" { | ||||||
| 		name = props["name"] | 			name = props["name"] | ||||||
| 	} else { |  | ||||||
| 		name = link |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	name += tail |  | ||||||
| 	image := false |  | ||||||
| 	switch ext := filepath.Ext(link); ext { |  | ||||||
| 	// fast path: empty string, ignore
 |  | ||||||
| 	case "": |  | ||||||
| 		break |  | ||||||
| 	case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg": |  | ||||||
| 		image = true |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	childNode := &html.Node{} |  | ||||||
| 	linkNode := &html.Node{ |  | ||||||
| 		FirstChild: childNode, |  | ||||||
| 		LastChild:  childNode, |  | ||||||
| 		Type:       html.ElementNode, |  | ||||||
| 		Data:       "a", |  | ||||||
| 		DataAtom:   atom.A, |  | ||||||
| 	} |  | ||||||
| 	childNode.Parent = linkNode |  | ||||||
| 	absoluteLink := isLinkStr(link) |  | ||||||
| 	if !absoluteLink { |  | ||||||
| 		if image { |  | ||||||
| 			link = strings.ReplaceAll(link, " ", "+") |  | ||||||
| 		} else { | 		} else { | ||||||
| 			link = strings.ReplaceAll(link, " ", "-") | 			name = link | ||||||
| 		} |  | ||||||
| 		if !strings.Contains(link, "/") { |  | ||||||
| 			link = url.PathEscape(link) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	urlPrefix := ctx.URLPrefix |  | ||||||
| 	if image { |  | ||||||
| 		if !absoluteLink { |  | ||||||
| 			if IsSameDomain(urlPrefix) { |  | ||||||
| 				urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) |  | ||||||
| 			} |  | ||||||
| 			if ctx.IsWiki { |  | ||||||
| 				link = util.URLJoin("wiki", "raw", link) |  | ||||||
| 			} |  | ||||||
| 			link = util.URLJoin(urlPrefix, link) |  | ||||||
| 		} |  | ||||||
| 		title := props["title"] |  | ||||||
| 		if title == "" { |  | ||||||
| 			title = props["alt"] |  | ||||||
| 		} |  | ||||||
| 		if title == "" { |  | ||||||
| 			title = path.Base(name) |  | ||||||
| 		} |  | ||||||
| 		alt := props["alt"] |  | ||||||
| 		if alt == "" { |  | ||||||
| 			alt = name |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// make the childNode an image - if we can, we also place the alt
 | 		name += tail | ||||||
| 		childNode.Type = html.ElementNode | 		image := false | ||||||
| 		childNode.Data = "img" | 		switch ext := filepath.Ext(link); ext { | ||||||
| 		childNode.DataAtom = atom.Img | 		// fast path: empty string, ignore
 | ||||||
| 		childNode.Attr = []html.Attribute{ | 		case "": | ||||||
| 			{Key: "src", Val: link}, | 			// leave image as false
 | ||||||
| 			{Key: "title", Val: title}, | 		case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg": | ||||||
| 			{Key: "alt", Val: alt}, | 			image = true | ||||||
| 		} | 		} | ||||||
| 		if alt == "" { | 
 | ||||||
| 			childNode.Attr = childNode.Attr[:2] | 		childNode := &html.Node{} | ||||||
|  | 		linkNode := &html.Node{ | ||||||
|  | 			FirstChild: childNode, | ||||||
|  | 			LastChild:  childNode, | ||||||
|  | 			Type:       html.ElementNode, | ||||||
|  | 			Data:       "a", | ||||||
|  | 			DataAtom:   atom.A, | ||||||
| 		} | 		} | ||||||
| 	} else { | 		childNode.Parent = linkNode | ||||||
|  | 		absoluteLink := isLinkStr(link) | ||||||
| 		if !absoluteLink { | 		if !absoluteLink { | ||||||
| 			if ctx.IsWiki { | 			if image { | ||||||
| 				link = util.URLJoin("wiki", link) | 				link = strings.ReplaceAll(link, " ", "+") | ||||||
|  | 			} else { | ||||||
|  | 				link = strings.ReplaceAll(link, " ", "-") | ||||||
|  | 			} | ||||||
|  | 			if !strings.Contains(link, "/") { | ||||||
|  | 				link = url.PathEscape(link) | ||||||
| 			} | 			} | ||||||
| 			link = util.URLJoin(urlPrefix, link) |  | ||||||
| 		} | 		} | ||||||
| 		childNode.Type = html.TextNode | 		urlPrefix := ctx.URLPrefix | ||||||
| 		childNode.Data = name | 		if image { | ||||||
|  | 			if !absoluteLink { | ||||||
|  | 				if IsSameDomain(urlPrefix) { | ||||||
|  | 					urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) | ||||||
|  | 				} | ||||||
|  | 				if ctx.IsWiki { | ||||||
|  | 					link = util.URLJoin("wiki", "raw", link) | ||||||
|  | 				} | ||||||
|  | 				link = util.URLJoin(urlPrefix, link) | ||||||
|  | 			} | ||||||
|  | 			title := props["title"] | ||||||
|  | 			if title == "" { | ||||||
|  | 				title = props["alt"] | ||||||
|  | 			} | ||||||
|  | 			if title == "" { | ||||||
|  | 				title = path.Base(name) | ||||||
|  | 			} | ||||||
|  | 			alt := props["alt"] | ||||||
|  | 			if alt == "" { | ||||||
|  | 				alt = name | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// make the childNode an image - if we can, we also place the alt
 | ||||||
|  | 			childNode.Type = html.ElementNode | ||||||
|  | 			childNode.Data = "img" | ||||||
|  | 			childNode.DataAtom = atom.Img | ||||||
|  | 			childNode.Attr = []html.Attribute{ | ||||||
|  | 				{Key: "src", Val: link}, | ||||||
|  | 				{Key: "title", Val: title}, | ||||||
|  | 				{Key: "alt", Val: alt}, | ||||||
|  | 			} | ||||||
|  | 			if alt == "" { | ||||||
|  | 				childNode.Attr = childNode.Attr[:2] | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			if !absoluteLink { | ||||||
|  | 				if ctx.IsWiki { | ||||||
|  | 					link = util.URLJoin("wiki", link) | ||||||
|  | 				} | ||||||
|  | 				link = util.URLJoin(urlPrefix, link) | ||||||
|  | 			} | ||||||
|  | 			childNode.Type = html.TextNode | ||||||
|  | 			childNode.Data = name | ||||||
|  | 		} | ||||||
|  | 		if noLink { | ||||||
|  | 			linkNode = childNode | ||||||
|  | 		} else { | ||||||
|  | 			linkNode.Attr = []html.Attribute{{Key: "href", Val: link}} | ||||||
|  | 		} | ||||||
|  | 		replaceContent(node, m[0], m[1], linkNode) | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
| 	} | 	} | ||||||
| 	if noLink { |  | ||||||
| 		linkNode = childNode |  | ||||||
| 	} else { |  | ||||||
| 		linkNode.Attr = []html.Attribute{{Key: "href", Val: link}} |  | ||||||
| 	} |  | ||||||
| 	replaceContent(node, m[0], m[1], linkNode) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { | func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.Metas == nil { | 	if ctx.Metas == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) |  | ||||||
| 	if m == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	link := node.Data[m[0]:m[1]] |  | ||||||
| 	id := "#" + node.Data[m[2]:m[3]] |  | ||||||
| 
 | 
 | ||||||
| 	// extract repo and org name from matched link like
 | 	next := node.NextSibling | ||||||
| 	// http://localhost:3000/gituser/myrepo/issues/1
 | 	for node != nil && node != next { | ||||||
| 	linkParts := strings.Split(path.Clean(link), "/") | 		m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) | ||||||
| 	matchOrg := linkParts[len(linkParts)-4] | 		if m == nil { | ||||||
| 	matchRepo := linkParts[len(linkParts)-3] | 			return | ||||||
|  | 		} | ||||||
|  | 		link := node.Data[m[0]:m[1]] | ||||||
|  | 		id := "#" + node.Data[m[2]:m[3]] | ||||||
| 
 | 
 | ||||||
| 	if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] { | 		// extract repo and org name from matched link like
 | ||||||
| 		// TODO if m[4]:m[5] is not nil, then link is to a comment,
 | 		// http://localhost:3000/gituser/myrepo/issues/1
 | ||||||
| 		// and we should indicate that in the text somehow
 | 		linkParts := strings.Split(path.Clean(link), "/") | ||||||
| 		replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue")) | 		matchOrg := linkParts[len(linkParts)-4] | ||||||
|  | 		matchRepo := linkParts[len(linkParts)-3] | ||||||
| 
 | 
 | ||||||
| 	} else { | 		if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] { | ||||||
| 		orgRepoID := matchOrg + "/" + matchRepo + id | 			// TODO if m[4]:m[5] is not nil, then link is to a comment,
 | ||||||
| 		replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue")) | 			// and we should indicate that in the text somehow
 | ||||||
|  | 			replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue")) | ||||||
|  | 		} else { | ||||||
|  | 			orgRepoID := matchOrg + "/" + matchRepo + id | ||||||
|  | 			replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue")) | ||||||
|  | 		} | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -782,70 +803,74 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.Metas == nil { | 	if ctx.Metas == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	var ( | 	var ( | ||||||
| 		found bool | 		found bool | ||||||
| 		ref   *references.RenderizableReference | 		ref   *references.RenderizableReference | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
| 	_, exttrack := ctx.Metas["format"] | 	next := node.NextSibling | ||||||
| 	alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric | 	for node != nil && node != next { | ||||||
|  | 		_, exttrack := ctx.Metas["format"] | ||||||
|  | 		alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric | ||||||
| 
 | 
 | ||||||
| 	// Repos with external issue trackers might still need to reference local PRs
 | 		// Repos with external issue trackers might still need to reference local PRs
 | ||||||
| 	// We need to concern with the first one that shows up in the text, whichever it is
 | 		// We need to concern with the first one that shows up in the text, whichever it is
 | ||||||
| 	found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum) | 		found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum) | ||||||
| 	if exttrack && alphanum { | 		if exttrack && alphanum { | ||||||
| 		if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 { | 			if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 { | ||||||
| 			if !found || ref2.RefLocation.Start < ref.RefLocation.Start { | 				if !found || ref2.RefLocation.Start < ref.RefLocation.Start { | ||||||
| 				found = true | 					found = true | ||||||
| 				ref = ref2 | 					ref = ref2 | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 		if !found { | ||||||
| 	if !found { | 			return | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var link *html.Node |  | ||||||
| 	reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] |  | ||||||
| 	if exttrack && !ref.IsPull { |  | ||||||
| 		ctx.Metas["index"] = ref.Issue |  | ||||||
| 		link = createLink(com.Expand(ctx.Metas["format"], ctx.Metas), reftext, "ref-issue") |  | ||||||
| 	} else { |  | ||||||
| 		// Path determines the type of link that will be rendered. It's unknown at this point whether
 |  | ||||||
| 		// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
 |  | ||||||
| 		// Gitea will redirect on click as appropriate.
 |  | ||||||
| 		path := "issues" |  | ||||||
| 		if ref.IsPull { |  | ||||||
| 			path = "pulls" |  | ||||||
| 		} | 		} | ||||||
| 		if ref.Owner == "" { | 
 | ||||||
| 			link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue") | 		var link *html.Node | ||||||
|  | 		reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] | ||||||
|  | 		if exttrack && !ref.IsPull { | ||||||
|  | 			ctx.Metas["index"] = ref.Issue | ||||||
|  | 			link = createLink(com.Expand(ctx.Metas["format"], ctx.Metas), reftext, "ref-issue") | ||||||
| 		} else { | 		} else { | ||||||
| 			link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue") | 			// Path determines the type of link that will be rendered. It's unknown at this point whether
 | ||||||
|  | 			// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
 | ||||||
|  | 			// Gitea will redirect on click as appropriate.
 | ||||||
|  | 			path := "issues" | ||||||
|  | 			if ref.IsPull { | ||||||
|  | 				path = "pulls" | ||||||
|  | 			} | ||||||
|  | 			if ref.Owner == "" { | ||||||
|  | 				link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue") | ||||||
|  | 			} else { | ||||||
|  | 				link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue") | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if ref.Action == references.XRefActionNone { | 		if ref.Action == references.XRefActionNone { | ||||||
| 		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) | 			replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) | ||||||
| 		return | 			node = node.NextSibling.NextSibling | ||||||
| 	} | 			continue | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 	// Decorate action keywords if actionable
 | 		// Decorate action keywords if actionable
 | ||||||
| 	var keyword *html.Node | 		var keyword *html.Node | ||||||
| 	if references.IsXrefActionable(ref, exttrack, alphanum) { | 		if references.IsXrefActionable(ref, exttrack, alphanum) { | ||||||
| 		keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) | 			keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) | ||||||
| 	} else { | 		} else { | ||||||
| 		keyword = &html.Node{ | 			keyword = &html.Node{ | ||||||
|  | 				Type: html.TextNode, | ||||||
|  | 				Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End], | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		spaces := &html.Node{ | ||||||
| 			Type: html.TextNode, | 			Type: html.TextNode, | ||||||
| 			Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End], | 			Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start], | ||||||
| 		} | 		} | ||||||
|  | 		replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link}) | ||||||
|  | 		node = node.NextSibling.NextSibling.NextSibling.NextSibling | ||||||
| 	} | 	} | ||||||
| 	spaces := &html.Node{ |  | ||||||
| 		Type: html.TextNode, |  | ||||||
| 		Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start], |  | ||||||
| 	} |  | ||||||
| 	replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link}) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // fullSha1PatternProcessor renders SHA containing URLs
 | // fullSha1PatternProcessor renders SHA containing URLs
 | ||||||
|  | @ -853,86 +878,112 @@ func fullSha1PatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.Metas == nil { | 	if ctx.Metas == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	m := anySHA1Pattern.FindStringSubmatchIndex(node.Data) |  | ||||||
| 	if m == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	urlFull := node.Data[m[0]:m[1]] | 	next := node.NextSibling | ||||||
| 	text := base.ShortSha(node.Data[m[2]:m[3]]) | 	for node != nil && node != next { | ||||||
| 
 | 		m := anySHA1Pattern.FindStringSubmatchIndex(node.Data) | ||||||
| 	// 3rd capture group matches a optional path
 | 		if m == nil { | ||||||
| 	subpath := "" | 			return | ||||||
| 	if m[5] > 0 { |  | ||||||
| 		subpath = node.Data[m[4]:m[5]] |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// 4th capture group matches a optional url hash
 |  | ||||||
| 	hash := "" |  | ||||||
| 	if m[7] > 0 { |  | ||||||
| 		hash = node.Data[m[6]:m[7]][1:] |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	start := m[0] |  | ||||||
| 	end := m[1] |  | ||||||
| 
 |  | ||||||
| 	// If url ends in '.', it's very likely that it is not part of the
 |  | ||||||
| 	// actual url but used to finish a sentence.
 |  | ||||||
| 	if strings.HasSuffix(urlFull, ".") { |  | ||||||
| 		end-- |  | ||||||
| 		urlFull = urlFull[:len(urlFull)-1] |  | ||||||
| 		if hash != "" { |  | ||||||
| 			hash = hash[:len(hash)-1] |  | ||||||
| 		} else if subpath != "" { |  | ||||||
| 			subpath = subpath[:len(subpath)-1] |  | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if subpath != "" { | 		urlFull := node.Data[m[0]:m[1]] | ||||||
| 		text += subpath | 		text := base.ShortSha(node.Data[m[2]:m[3]]) | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if hash != "" { | 		// 3rd capture group matches a optional path
 | ||||||
| 		text += " (" + hash + ")" | 		subpath := "" | ||||||
| 	} | 		if m[5] > 0 { | ||||||
|  | 			subpath = node.Data[m[4]:m[5]] | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 	replaceContent(node, start, end, createCodeLink(urlFull, text, "commit")) | 		// 4th capture group matches a optional url hash
 | ||||||
|  | 		hash := "" | ||||||
|  | 		if m[7] > 0 { | ||||||
|  | 			hash = node.Data[m[6]:m[7]][1:] | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		start := m[0] | ||||||
|  | 		end := m[1] | ||||||
|  | 
 | ||||||
|  | 		// If url ends in '.', it's very likely that it is not part of the
 | ||||||
|  | 		// actual url but used to finish a sentence.
 | ||||||
|  | 		if strings.HasSuffix(urlFull, ".") { | ||||||
|  | 			end-- | ||||||
|  | 			urlFull = urlFull[:len(urlFull)-1] | ||||||
|  | 			if hash != "" { | ||||||
|  | 				hash = hash[:len(hash)-1] | ||||||
|  | 			} else if subpath != "" { | ||||||
|  | 				subpath = subpath[:len(subpath)-1] | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if subpath != "" { | ||||||
|  | 			text += subpath | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if hash != "" { | ||||||
|  | 			text += " (" + hash + ")" | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		replaceContent(node, start, end, createCodeLink(urlFull, text, "commit")) | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // emojiShortCodeProcessor for rendering text like :smile: into emoji
 | // emojiShortCodeProcessor for rendering text like :smile: into emoji
 | ||||||
| func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { | func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data) | 	start := 0 | ||||||
| 	if m == nil { | 	next := node.NextSibling | ||||||
| 		return | 	for node != nil && node != next && start < len(node.Data) { | ||||||
| 	} | 		m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:]) | ||||||
| 
 | 		if m == nil { | ||||||
| 	alias := node.Data[m[0]:m[1]] |  | ||||||
| 	alias = strings.ReplaceAll(alias, ":", "") |  | ||||||
| 	converted := emoji.FromAlias(alias) |  | ||||||
| 	if converted == nil { |  | ||||||
| 		// check if this is a custom reaction
 |  | ||||||
| 		s := strings.Join(setting.UI.Reactions, " ") + "gitea" |  | ||||||
| 		if strings.Contains(s, alias) { |  | ||||||
| 			replaceContent(node, m[0], m[1], createCustomEmoji(alias, "emoji")) |  | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		return | 		m[0] += start | ||||||
| 	} | 		m[1] += start | ||||||
| 
 | 
 | ||||||
| 	replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description)) | 		start = m[1] | ||||||
|  | 
 | ||||||
|  | 		alias := node.Data[m[0]:m[1]] | ||||||
|  | 		alias = strings.ReplaceAll(alias, ":", "") | ||||||
|  | 		converted := emoji.FromAlias(alias) | ||||||
|  | 		if converted == nil { | ||||||
|  | 			// check if this is a custom reaction
 | ||||||
|  | 			s := strings.Join(setting.UI.Reactions, " ") + "gitea" | ||||||
|  | 			if strings.Contains(s, alias) { | ||||||
|  | 				replaceContent(node, m[0], m[1], createCustomEmoji(alias, "emoji")) | ||||||
|  | 				node = node.NextSibling.NextSibling | ||||||
|  | 				start = 0 | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description)) | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
|  | 		start = 0 | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // emoji processor to match emoji and add emoji class
 | // emoji processor to match emoji and add emoji class
 | ||||||
| func emojiProcessor(ctx *RenderContext, node *html.Node) { | func emojiProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := emoji.FindEmojiSubmatchIndex(node.Data) | 	start := 0 | ||||||
| 	if m == nil { | 	next := node.NextSibling | ||||||
| 		return | 	for node != nil && node != next && start < len(node.Data) { | ||||||
| 	} | 		m := emoji.FindEmojiSubmatchIndex(node.Data[start:]) | ||||||
|  | 		if m == nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		m[0] += start | ||||||
|  | 		m[1] += start | ||||||
| 
 | 
 | ||||||
| 	codepoint := node.Data[m[0]:m[1]] | 		codepoint := node.Data[m[0]:m[1]] | ||||||
| 	val := emoji.FromCode(codepoint) | 		start = m[1] | ||||||
| 	if val != nil { | 		val := emoji.FromCode(codepoint) | ||||||
| 		replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description)) | 		if val != nil { | ||||||
|  | 			replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description)) | ||||||
|  | 			node = node.NextSibling.NextSibling | ||||||
|  | 			start = 0 | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -942,49 +993,70 @@ func sha1CurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" { | 	if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data) |  | ||||||
| 	if m == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	hash := node.Data[m[2]:m[3]] |  | ||||||
| 	// The regex does not lie, it matches the hash pattern.
 |  | ||||||
| 	// However, a regex cannot know if a hash actually exists or not.
 |  | ||||||
| 	// We could assume that a SHA1 hash should probably contain alphas AND numerics
 |  | ||||||
| 	// but that is not always the case.
 |  | ||||||
| 	// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
 |  | ||||||
| 	// as used by git and github for linking and thus we have to do similar.
 |  | ||||||
| 	// Because of this, we check to make sure that a matched hash is actually
 |  | ||||||
| 	// a commit in the repository before making it a link.
 |  | ||||||
| 	if _, err := git.NewCommand("rev-parse", "--verify", hash).RunInDirBytes(ctx.Metas["repoPath"]); err != nil { |  | ||||||
| 		if !strings.Contains(err.Error(), "fatal: Needed a single revision") { |  | ||||||
| 			log.Debug("sha1CurrentPatternProcessor git rev-parse: %v", err) |  | ||||||
| 		} |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	replaceContent(node, m[2], m[3], | 	start := 0 | ||||||
| 		createCodeLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash), base.ShortSha(hash), "commit")) | 	next := node.NextSibling | ||||||
|  | 	for node != nil && node != next && start < len(node.Data) { | ||||||
|  | 		m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data[start:]) | ||||||
|  | 		if m == nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		m[2] += start | ||||||
|  | 		m[3] += start | ||||||
|  | 
 | ||||||
|  | 		hash := node.Data[m[2]:m[3]] | ||||||
|  | 		// The regex does not lie, it matches the hash pattern.
 | ||||||
|  | 		// However, a regex cannot know if a hash actually exists or not.
 | ||||||
|  | 		// We could assume that a SHA1 hash should probably contain alphas AND numerics
 | ||||||
|  | 		// but that is not always the case.
 | ||||||
|  | 		// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
 | ||||||
|  | 		// as used by git and github for linking and thus we have to do similar.
 | ||||||
|  | 		// Because of this, we check to make sure that a matched hash is actually
 | ||||||
|  | 		// a commit in the repository before making it a link.
 | ||||||
|  | 		if _, err := git.NewCommand("rev-parse", "--verify", hash).RunInDirBytes(ctx.Metas["repoPath"]); err != nil { | ||||||
|  | 			if !strings.Contains(err.Error(), "fatal: Needed a single revision") { | ||||||
|  | 				log.Debug("sha1CurrentPatternProcessor git rev-parse: %v", err) | ||||||
|  | 			} | ||||||
|  | 			start = m[3] | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		replaceContent(node, m[2], m[3], | ||||||
|  | 			createCodeLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash), base.ShortSha(hash), "commit")) | ||||||
|  | 		start = 0 | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // emailAddressProcessor replaces raw email addresses with a mailto: link.
 | // emailAddressProcessor replaces raw email addresses with a mailto: link.
 | ||||||
| func emailAddressProcessor(ctx *RenderContext, node *html.Node) { | func emailAddressProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := emailRegex.FindStringSubmatchIndex(node.Data) | 	next := node.NextSibling | ||||||
| 	if m == nil { | 	for node != nil && node != next { | ||||||
| 		return | 		m := emailRegex.FindStringSubmatchIndex(node.Data) | ||||||
|  | 		if m == nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		mail := node.Data[m[2]:m[3]] | ||||||
|  | 		replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto")) | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
| 	} | 	} | ||||||
| 	mail := node.Data[m[2]:m[3]] |  | ||||||
| 	replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto")) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // linkProcessor creates links for any HTTP or HTTPS URL not captured by
 | // linkProcessor creates links for any HTTP or HTTPS URL not captured by
 | ||||||
| // markdown.
 | // markdown.
 | ||||||
| func linkProcessor(ctx *RenderContext, node *html.Node) { | func linkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := common.LinkRegex.FindStringIndex(node.Data) | 	next := node.NextSibling | ||||||
| 	if m == nil { | 	for node != nil && node != next { | ||||||
| 		return | 		m := common.LinkRegex.FindStringIndex(node.Data) | ||||||
|  | 		if m == nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		uri := node.Data[m[0]:m[1]] | ||||||
|  | 		replaceContent(node, m[0], m[1], createLink(uri, uri, "link")) | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
| 	} | 	} | ||||||
| 	uri := node.Data[m[0]:m[1]] |  | ||||||
| 	replaceContent(node, m[0], m[1], createLink(uri, uri, "link")) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func genDefaultLinkProcessor(defaultLink string) processor { | func genDefaultLinkProcessor(defaultLink string) processor { | ||||||
|  | @ -1008,12 +1080,17 @@ func genDefaultLinkProcessor(defaultLink string) processor { | ||||||
| 
 | 
 | ||||||
| // descriptionLinkProcessor creates links for DescriptionHTML
 | // descriptionLinkProcessor creates links for DescriptionHTML
 | ||||||
| func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) { | func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := common.LinkRegex.FindStringIndex(node.Data) | 	next := node.NextSibling | ||||||
| 	if m == nil { | 	for node != nil && node != next { | ||||||
| 		return | 		m := common.LinkRegex.FindStringIndex(node.Data) | ||||||
|  | 		if m == nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		uri := node.Data[m[0]:m[1]] | ||||||
|  | 		replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri)) | ||||||
|  | 		node = node.NextSibling.NextSibling | ||||||
| 	} | 	} | ||||||
| 	uri := node.Data[m[0]:m[1]] |  | ||||||
| 	replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri)) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func createDescriptionLink(href, content string) *html.Node { | func createDescriptionLink(href, content string) *html.Node { | ||||||
|  |  | ||||||
|  | @ -464,3 +464,19 @@ func TestIssue16020(t *testing.T) { | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, data, res.String()) | 	assert.Equal(t, data, res.String()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func BenchmarkEmojiPostprocess(b *testing.B) { | ||||||
|  | 	data := "🥰 " | ||||||
|  | 	for len(data) < 1<<16 { | ||||||
|  | 		data += data | ||||||
|  | 	} | ||||||
|  | 	b.ResetTimer() | ||||||
|  | 	for i := 0; i < b.N; i++ { | ||||||
|  | 		var res strings.Builder | ||||||
|  | 		err := PostProcess(&RenderContext{ | ||||||
|  | 			URLPrefix: "https://example.com", | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 		}, strings.NewReader(data), &res) | ||||||
|  | 		assert.NoError(b, err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue