mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Refactor markup render to fix various path problems (#34114)
* Fix #33972 * Use consistent path resolving for links and medias. * No need to make the markup renders to resolve the paths, instead, the paths are all correctly resolved in the "post process" step. * Fix #33274 * Since 1.23, all paths starting with "/" are relative to current render context (for example: the current repo branch) * Introduce `/:root/path-relative-to-root`, then the path will be rendered as relative to "ROOT_URL"
This commit is contained in:
		| @@ -28,14 +28,14 @@ func (r *RepoComment) IsCommitIDExisting(commitID string) bool { | ||||
| 	return r.commitChecker.IsCommitIDExisting(commitID) | ||||
| } | ||||
|  | ||||
| func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) { | ||||
| 	switch likeType { | ||||
| 	case markup.LinkTypeApp: | ||||
| 		finalLink = r.ctx.ResolveLinkApp(link) | ||||
| func (r *RepoComment) ResolveLink(link, preferLinkType string) string { | ||||
| 	linkType, link := markup.ParseRenderedLink(link, preferLinkType) | ||||
| 	switch linkType { | ||||
| 	case markup.LinkTypeRoot: | ||||
| 		return r.ctx.ResolveLinkRoot(link) | ||||
| 	default: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link) | ||||
| 		return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link) | ||||
| 	} | ||||
| 	return finalLink | ||||
| } | ||||
|  | ||||
| var _ markup.RenderHelper = (*RepoComment)(nil) | ||||
|   | ||||
| @@ -29,17 +29,17 @@ func (r *RepoFile) IsCommitIDExisting(commitID string) bool { | ||||
| 	return r.commitChecker.IsCommitIDExisting(commitID) | ||||
| } | ||||
|  | ||||
| func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string { | ||||
| 	finalLink := link | ||||
| 	switch likeType { | ||||
| 	case markup.LinkTypeApp: | ||||
| 		finalLink = r.ctx.ResolveLinkApp(link) | ||||
| 	case markup.LinkTypeDefault: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) | ||||
| func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) { | ||||
| 	linkType, link := markup.ParseRenderedLink(link, preferLinkType) | ||||
| 	switch linkType { | ||||
| 	case markup.LinkTypeRoot: | ||||
| 		finalLink = r.ctx.ResolveLinkRoot(link) | ||||
| 	case markup.LinkTypeRaw: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) | ||||
| 	case markup.LinkTypeMedia: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) | ||||
| 	default: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) | ||||
| 	} | ||||
| 	return finalLink | ||||
| } | ||||
|   | ||||
| @@ -48,8 +48,8 @@ func TestRepoFile(t *testing.T) { | ||||
| 		assert.Equal(t, | ||||
| 			`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a> | ||||
| <a href="/user2/repo1/src/branch/main/test" rel="nofollow">./test</a> | ||||
| <a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a> | ||||
| <a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p> | ||||
| <a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a> | ||||
| <a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p> | ||||
| `, rendered) | ||||
| 	}) | ||||
|  | ||||
| @@ -62,7 +62,7 @@ func TestRepoFile(t *testing.T) { | ||||
| `) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a> | ||||
| <a href="/user2/repo1/media/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p> | ||||
| <a href="/user2/repo1/src/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p> | ||||
| `, rendered) | ||||
| 	}) | ||||
|  | ||||
| @@ -77,7 +77,7 @@ func TestRepoFile(t *testing.T) { | ||||
| <video src="LINK"> | ||||
| `) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, `<a href="/user2/repo1/media/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a> | ||||
| 		assert.Equal(t, `<a href="/user2/repo1/src/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a> | ||||
| <video src="/user2/repo1/media/commit/1234/my-dir/LINK"> | ||||
| </video>`, rendered) | ||||
| 	}) | ||||
| @@ -100,7 +100,7 @@ func TestRepoFileOrgMode(t *testing.T) { | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, `<p> | ||||
| <a href="https://google.com/" rel="nofollow">https://google.com/</a> | ||||
| <a href="/user2/repo1/media/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p> | ||||
| <a href="/user2/repo1/src/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p> | ||||
| `, rendered) | ||||
| 	}) | ||||
|  | ||||
|   | ||||
| @@ -30,18 +30,16 @@ func (r *RepoWiki) IsCommitIDExisting(commitID string) bool { | ||||
| 	return r.commitChecker.IsCommitIDExisting(commitID) | ||||
| } | ||||
|  | ||||
| func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string { | ||||
| 	finalLink := link | ||||
| 	switch likeType { | ||||
| 	case markup.LinkTypeApp: | ||||
| 		finalLink = r.ctx.ResolveLinkApp(link) | ||||
| 	case markup.LinkTypeDefault: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link) | ||||
| 	case markup.LinkTypeMedia: | ||||
| func (r *RepoWiki) ResolveLink(link, preferLinkType string) (finalLink string) { | ||||
| 	linkType, link := markup.ParseRenderedLink(link, preferLinkType) | ||||
| 	switch linkType { | ||||
| 	case markup.LinkTypeRoot: | ||||
| 		finalLink = r.ctx.ResolveLinkRoot(link) | ||||
| 	case markup.LinkTypeMedia, markup.LinkTypeRaw: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link) | ||||
| 	case markup.LinkTypeRaw: // wiki doesn't use it | ||||
| 	default: | ||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link) | ||||
| 	} | ||||
|  | ||||
| 	return finalLink | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -45,8 +45,8 @@ func TestRepoWiki(t *testing.T) { | ||||
| 		assert.Equal(t, | ||||
| 			`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a> | ||||
| <a href="/user2/repo1/wiki/test" rel="nofollow">./test</a> | ||||
| <a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a> | ||||
| <a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p> | ||||
| <a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a> | ||||
| <a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p> | ||||
| `, rendered) | ||||
| 	}) | ||||
|  | ||||
| @@ -57,7 +57,7 @@ func TestRepoWiki(t *testing.T) { | ||||
| <video src="LINK"> | ||||
| `) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, `<a href="/user2/repo1/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a> | ||||
| 		assert.Equal(t, `<a href="/user2/repo1/wiki/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a> | ||||
| <video src="/user2/repo1/wiki/raw/LINK"> | ||||
| </video>`, rendered) | ||||
| 	}) | ||||
|   | ||||
| @@ -15,9 +15,15 @@ type SimpleDocument struct { | ||||
| 	baseLink string | ||||
| } | ||||
|  | ||||
| func (r *SimpleDocument) ResolveLink(link string, likeType markup.LinkType) string { | ||||
| func (r *SimpleDocument) ResolveLink(link, preferLinkType string) string { | ||||
| 	linkType, link := markup.ParseRenderedLink(link, preferLinkType) | ||||
| 	switch linkType { | ||||
| 	case markup.LinkTypeRoot: | ||||
| 		return r.ctx.ResolveLinkRoot(link) | ||||
| 	default: | ||||
| 		return r.ctx.ResolveLinkRelative(r.baseLink, "", link) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var _ markup.RenderHelper = (*SimpleDocument)(nil) | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,7 @@ func TestSimpleDocument(t *testing.T) { | ||||
| 	assert.Equal(t, | ||||
| 		`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d | ||||
| #1 | ||||
| <a href="/base/user2" rel="nofollow">@user2</a></p> | ||||
| <a href="/user2" rel="nofollow">@user2</a></p> | ||||
| <p><a href="/base/test" rel="nofollow">/test</a> | ||||
| <a href="/base/test" rel="nofollow">./test</a> | ||||
| <a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="/image"/></a> | ||||
|   | ||||
							
								
								
									
										20
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							| @@ -77,14 +77,14 @@ func envMark(envName string) string { | ||||
|  | ||||
| // Render renders the data of the document to HTML via the external tool. | ||||
| func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||
| 	var ( | ||||
| 		command = strings.NewReplacer( | ||||
| 			envMark("GITEA_PREFIX_SRC"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault), | ||||
| 			envMark("GITEA_PREFIX_RAW"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw), | ||||
| 	baseLinkSrc := ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault) | ||||
| 	baseLinkRaw := ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw) | ||||
| 	command := strings.NewReplacer( | ||||
| 		envMark("GITEA_PREFIX_SRC"), baseLinkSrc, | ||||
| 		envMark("GITEA_PREFIX_RAW"), baseLinkRaw, | ||||
| 	).Replace(p.Command) | ||||
| 		commands = strings.Fields(command) | ||||
| 		args     = commands[1:] | ||||
| 	) | ||||
| 	commands := strings.Fields(command) | ||||
| 	args := commands[1:] | ||||
|  | ||||
| 	if p.IsInputFile { | ||||
| 		// write to temp file | ||||
| @@ -112,14 +112,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io. | ||||
| 		args = append(args, f.Name()) | ||||
| 	} | ||||
|  | ||||
| 	processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault))) | ||||
| 	processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], baseLinkSrc)) | ||||
| 	defer finished() | ||||
|  | ||||
| 	cmd := exec.CommandContext(processCtx, commands[0], args...) | ||||
| 	cmd.Env = append( | ||||
| 		os.Environ(), | ||||
| 		"GITEA_PREFIX_SRC="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault), | ||||
| 		"GITEA_PREFIX_RAW="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw), | ||||
| 		"GITEA_PREFIX_SRC="+baseLinkSrc, | ||||
| 		"GITEA_PREFIX_RAW="+baseLinkRaw, | ||||
| 	) | ||||
| 	if !p.IsInputFile { | ||||
| 		cmd.Stdin = input | ||||
|   | ||||
| @@ -32,7 +32,6 @@ type globalVarsType struct { | ||||
| 	comparePattern          *regexp.Regexp | ||||
| 	fullURLPattern          *regexp.Regexp | ||||
| 	emailRegex              *regexp.Regexp | ||||
| 	blackfridayExtRegex     *regexp.Regexp | ||||
| 	emojiShortCodeRegex     *regexp.Regexp | ||||
| 	issueFullPattern        *regexp.Regexp | ||||
| 	filesChangedFullPattern *regexp.Regexp | ||||
| @@ -74,9 +73,6 @@ var globalVars = sync.OnceValue(func() *globalVarsType { | ||||
| 	//   https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) | ||||
| 	v.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|$))") | ||||
|  | ||||
| 	// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote | ||||
| 	v.blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) | ||||
|  | ||||
| 	// emojiShortCodeRegex find emoji by alias like :smile: | ||||
| 	v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) | ||||
|  | ||||
| @@ -94,17 +90,12 @@ var globalVars = sync.OnceValue(func() *globalVarsType { | ||||
| 	return v | ||||
| }) | ||||
|  | ||||
| // IsFullURLBytes reports whether link fits valid format. | ||||
| func IsFullURLBytes(link []byte) bool { | ||||
| 	return globalVars().fullURLPattern.Match(link) | ||||
| } | ||||
|  | ||||
| func IsFullURLString(link string) bool { | ||||
| 	return globalVars().fullURLPattern.MatchString(link) | ||||
| } | ||||
|  | ||||
| func IsNonEmptyRelativePath(link string) bool { | ||||
| 	return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#' | ||||
| 	return link != "" && !IsFullURLString(link) && link[0] != '?' && link[0] != '#' | ||||
| } | ||||
|  | ||||
| // CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text | ||||
| @@ -316,27 +307,18 @@ func isEmojiNode(node *html.Node) bool { | ||||
| } | ||||
|  | ||||
| func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node { | ||||
| 	// Add user-content- to IDs and "#" links if they don't already have them | ||||
| 	for idx, attr := range node.Attr { | ||||
| 		val := strings.TrimPrefix(attr.Val, "#") | ||||
| 		notHasPrefix := !(strings.HasPrefix(val, "user-content-") || globalVars().blackfridayExtRegex.MatchString(val)) | ||||
|  | ||||
| 		if attr.Key == "id" && notHasPrefix { | ||||
| 			node.Attr[idx].Val = "user-content-" + attr.Val | ||||
| 		} | ||||
|  | ||||
| 		if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix { | ||||
| 			node.Attr[idx].Val = "#user-content-" + val | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	switch node.Type { | ||||
| 	case html.TextNode: | ||||
| 	if node.Type == html.TextNode { | ||||
| 		for _, proc := range procs { | ||||
| 			proc(ctx, node) // it might add siblings | ||||
| 		} | ||||
| 		return node.NextSibling | ||||
| 	} | ||||
| 	if node.Type != html.ElementNode { | ||||
| 		return node.NextSibling | ||||
| 	} | ||||
|  | ||||
| 	processNodeAttrID(node) | ||||
|  | ||||
| 	case html.ElementNode: | ||||
| 	if isEmojiNode(node) { | ||||
| 		// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span" | ||||
| 		// if we don't stop it, it will go into the TextNode again and create an infinite recursion | ||||
| @@ -347,14 +329,17 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod | ||||
| 		return visitNodeImg(ctx, node) | ||||
| 	} else if node.Data == "video" { | ||||
| 		return visitNodeVideo(ctx, node) | ||||
| 		} else if node.Data == "a" { | ||||
| 			procs = emojiProcessors // Restrict text in links to emojis | ||||
| 	} | ||||
|  | ||||
| 	if node.Data == "a" { | ||||
| 		processNodeA(ctx, node) | ||||
| 		// only use emoji processors for the content in the "A" tag, | ||||
| 		// because the content there is not processable, for example: the content is a commit id or a full URL. | ||||
| 		procs = emojiProcessors | ||||
| 	} | ||||
| 	for n := node.FirstChild; n != nil; { | ||||
| 		n = visitNode(ctx, procs, n) | ||||
| 	} | ||||
| 	default: | ||||
| 	} | ||||
| 	return node.NextSibling | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -43,7 +43,6 @@ func createCodeLink(href, content, class string) *html.Node { | ||||
| 	code := &html.Node{ | ||||
| 		Type: html.ElementNode, | ||||
| 		Data: atom.Code.String(), | ||||
| 		Attr: []html.Attribute{{Key: "class", Val: "nohighlight"}}, | ||||
| 	} | ||||
|  | ||||
| 	code.AppendChild(text) | ||||
| @@ -189,7 +188,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		link := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash), LinkTypeApp) | ||||
| 		link := "/:root/" + util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash) | ||||
| 		replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) | ||||
| 		start = 0 | ||||
| 		node = node.NextSibling.NextSibling | ||||
| @@ -205,9 +204,9 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) | ||||
| 		linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) | ||||
| 		link := createLink(ctx, linkHref, reftext, "commit") | ||||
| 		refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) | ||||
| 		linkHref := "/:root/" + util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha) | ||||
| 		link := createLink(ctx, linkHref, refText, "commit") | ||||
|  | ||||
| 		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) | ||||
| 		node = node.NextSibling.NextSibling | ||||
|   | ||||
| @@ -107,7 +107,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | ||||
| 		isExternal := false | ||||
| 		if marker == "!" { | ||||
| 			path = "pulls" | ||||
| 			prefix = "http://localhost:3000/someUser/someRepo/pulls/" | ||||
| 			prefix = "/someUser/someRepo/pulls/" | ||||
| 		} else { | ||||
| 			path = "issues" | ||||
| 			prefix = "https://someurl.com/someUser/someRepo/" | ||||
| @@ -116,7 +116,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | ||||
|  | ||||
| 		links := make([]any, len(indices)) | ||||
| 		for i, index := range indices { | ||||
| 			links[i] = numericIssueLink(util.URLJoin(TestRepoURL, path), "ref-issue", index, marker) | ||||
| 			links[i] = numericIssueLink(util.URLJoin("/test-owner/test-repo", path), "ref-issue", index, marker) | ||||
| 		} | ||||
| 		expectedNil := fmt.Sprintf(expectedFmt, links...) | ||||
| 		testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas)) | ||||
| @@ -293,13 +293,13 @@ func TestRender_AutoLink(t *testing.T) { | ||||
|  | ||||
| 	// render valid commit URLs | ||||
| 	tmp := util.URLJoin(TestRepoURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae") | ||||
| 	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24</code></a>") | ||||
| 	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>") | ||||
| 	tmp += "#diff-2" | ||||
| 	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>") | ||||
| 	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>") | ||||
|  | ||||
| 	// render other commit URLs | ||||
| 	tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2" | ||||
| 	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>") | ||||
| 	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>") | ||||
| } | ||||
|  | ||||
| func TestRender_FullIssueURLs(t *testing.T) { | ||||
|   | ||||
| @@ -82,7 +82,7 @@ func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref | ||||
| 	h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{ | ||||
| 		OwnerName:  ref.Owner, | ||||
| 		RepoName:   ref.Name, | ||||
| 		LinkHref:   linkHref, | ||||
| 		LinkHref:   ctx.RenderHelper.ResolveLink(linkHref, LinkTypeDefault), | ||||
| 		IssueIndex: issueIndex, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| @@ -162,7 +162,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 			issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) | ||||
| 			issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) | ||||
| 			issuePath := util.Iif(ref.IsPull, "pulls", "issues") | ||||
| 			linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp) | ||||
| 			linkHref := "/:root/" + util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue) | ||||
|  | ||||
| 			// at the moment, only render the issue index in a full line (or simple line) as icon+title | ||||
| 			// otherwise it would be too noisy for "take #1 as an example" in a sentence | ||||
|   | ||||
| @@ -39,7 +39,7 @@ func TestRender_IssueList(t *testing.T) { | ||||
| 	t.Run("NormalIssueRef", func(t *testing.T) { | ||||
| 		test( | ||||
| 			"#12345", | ||||
| 			`<p><a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`, | ||||
| 			`<p><a href="/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`, | ||||
| 		) | ||||
| 	}) | ||||
|  | ||||
| @@ -56,7 +56,7 @@ func TestRender_IssueList(t *testing.T) { | ||||
| 		test( | ||||
| 			"* foo #12345 bar", | ||||
| 			`<ul> | ||||
| <li>foo <a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li> | ||||
| <li>foo <a href="/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li> | ||||
| </ul>`, | ||||
| 		) | ||||
| 	}) | ||||
|   | ||||
| @@ -125,7 +125,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 			} | ||||
| 		} | ||||
| 		if image { | ||||
| 			link = ctx.RenderHelper.ResolveLink(link, LinkTypeMedia) | ||||
| 			title := props["title"] | ||||
| 			if title == "" { | ||||
| 				title = props["alt"] | ||||
| @@ -151,7 +150,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 				childNode.Attr = childNode.Attr[:2] | ||||
| 			} | ||||
| 		} else { | ||||
| 			link = ctx.RenderHelper.ResolveLink(link, LinkTypeDefault) | ||||
| 			childNode.Type = html.TextNode | ||||
| 			childNode.Data = name | ||||
| 		} | ||||
|   | ||||
| @@ -33,7 +33,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 		if ok && strings.Contains(mention, "/") { | ||||
| 			mentionOrgAndTeam := strings.Split(mention, "/") | ||||
| 			if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { | ||||
| 				link := ctx.RenderHelper.ResolveLink(util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]), LinkTypeApp) | ||||
| 				link := "/:root/" + util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]) | ||||
| 				replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) | ||||
| 				node = node.NextSibling.NextSibling | ||||
| 				start = 0 | ||||
| @@ -45,7 +45,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { | ||||
| 		mentionedUsername := mention[1:] | ||||
|  | ||||
| 		if DefaultRenderHelperFuncs != nil && DefaultRenderHelperFuncs.IsUsernameMentionable(ctx, mentionedUsername) { | ||||
| 			link := ctx.RenderHelper.ResolveLink(mentionedUsername, LinkTypeApp) | ||||
| 			link := "/:root/" + mentionedUsername | ||||
| 			replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) | ||||
| 			node = node.NextSibling.NextSibling | ||||
| 			start = 0 | ||||
|   | ||||
| @@ -4,42 +4,79 @@ | ||||
| package markup | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"golang.org/x/net/html" | ||||
| ) | ||||
|  | ||||
| func isAnchorIDUserContent(s string) bool { | ||||
| 	// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote | ||||
| 	// old logic: blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) | ||||
| 	return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") | ||||
| } | ||||
|  | ||||
| func processNodeAttrID(node *html.Node) { | ||||
| 	// Add user-content- to IDs and "#" links if they don't already have them, | ||||
| 	// and convert the link href to a relative link to the host root | ||||
| 	for idx, attr := range node.Attr { | ||||
| 		if attr.Key == "id" { | ||||
| 			if !isAnchorIDUserContent(attr.Val) { | ||||
| 				node.Attr[idx].Val = "user-content-" + attr.Val | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func processNodeA(ctx *RenderContext, node *html.Node) { | ||||
| 	for idx, attr := range node.Attr { | ||||
| 		if attr.Key == "href" { | ||||
| 			if anchorID, ok := strings.CutPrefix(attr.Val, "#"); ok { | ||||
| 				if !isAnchorIDUserContent(attr.Val) { | ||||
| 					node.Attr[idx].Val = "#user-content-" + anchorID | ||||
| 				} | ||||
| 			} else { | ||||
| 				node.Attr[idx].Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeDefault) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { | ||||
| 	next = img.NextSibling | ||||
| 	for i, attr := range img.Attr { | ||||
| 		if attr.Key != "src" { | ||||
| 	for i, imgAttr := range img.Attr { | ||||
| 		if imgAttr.Key != "src" { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if IsNonEmptyRelativePath(attr.Val) { | ||||
| 			attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia) | ||||
| 		imgSrcOrigin := imgAttr.Val | ||||
| 		isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:") | ||||
|  | ||||
| 		// By default, the "<img>" tag should also be clickable, | ||||
| 		// because frontend use `<img>` to paste the re-scaled image into the markdown, | ||||
| 		// so it must match the default markdown image behavior. | ||||
| 			hasParentAnchor := false | ||||
| 			for p := img.Parent; p != nil; p = p.Parent { | ||||
| 				if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor { | ||||
| 		cnt := 0 | ||||
| 		for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent { | ||||
| 			if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor { | ||||
| 				isLinkable = false | ||||
| 				break | ||||
| 			} | ||||
| 			cnt++ | ||||
| 		} | ||||
| 			if !hasParentAnchor { | ||||
| 				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{ | ||||
| 					{Key: "href", Val: attr.Val}, | ||||
| 		if isLinkable { | ||||
| 			wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{ | ||||
| 				{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)}, | ||||
| 				{Key: "target", Val: "_blank"}, | ||||
| 			}} | ||||
| 			parent := img.Parent | ||||
| 			imgNext := img.NextSibling | ||||
| 			parent.RemoveChild(img) | ||||
| 				parent.InsertBefore(imgA, imgNext) | ||||
| 				imgA.AppendChild(img) | ||||
| 			parent.InsertBefore(wrapper, imgNext) | ||||
| 			wrapper.AppendChild(img) | ||||
| 		} | ||||
| 		} | ||||
| 		attr.Val = camoHandleLink(attr.Val) | ||||
| 		img.Attr[i] = attr | ||||
|  | ||||
| 		imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia) | ||||
| 		imgAttr.Val = camoHandleLink(imgAttr.Val) | ||||
| 		img.Attr[i] = imgAttr | ||||
| 	} | ||||
| 	return next | ||||
| } | ||||
|   | ||||
| @@ -35,6 +35,7 @@ func TestRender_Commits(t *testing.T) { | ||||
| 	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" | ||||
| 	repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/" | ||||
| 	commit := util.URLJoin(repo, "commit", sha) | ||||
| 	commitPath := "/user13/repo11/commit/" + sha | ||||
| 	tree := util.URLJoin(repo, "tree", sha, "src") | ||||
|  | ||||
| 	file := util.URLJoin(repo, "commit", sha, "example.txt") | ||||
| @@ -44,9 +45,9 @@ func TestRender_Commits(t *testing.T) { | ||||
| 	commitCompare := util.URLJoin(repo, "compare", sha+"..."+sha) | ||||
| 	commitCompareWithHash := commitCompare + "#L2" | ||||
|  | ||||
| 	test(sha, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`) | ||||
| 	test(sha[:7], `<p><a href="`+commit[:len(commit)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`) | ||||
| 	test(sha[:39], `<p><a href="`+commit[:len(commit)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`) | ||||
| 	test(sha, `<p><a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`) | ||||
| 	test(sha[:7], `<p><a href="`+commitPath[:len(commitPath)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`) | ||||
| 	test(sha[:39], `<p><a href="`+commitPath[:len(commitPath)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`) | ||||
| 	test(commit, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`) | ||||
| 	test(tree, `<p><a href="`+tree+`" rel="nofollow"><code>65f1bf27bc/src</code></a></p>`) | ||||
|  | ||||
| @@ -57,13 +58,13 @@ func TestRender_Commits(t *testing.T) { | ||||
| 	test(commitCompare, `<p><a href="`+commitCompare+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code></a></p>`) | ||||
| 	test(commitCompareWithHash, `<p><a href="`+commitCompareWithHash+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc (L2)</code></a></p>`) | ||||
|  | ||||
| 	test("commit "+sha, `<p>commit <a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`) | ||||
| 	test("commit "+sha, `<p>commit <a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`) | ||||
| 	test("/home/gitea/"+sha, "<p>/home/gitea/"+sha+"</p>") | ||||
| 	test("deadbeef", `<p>deadbeef</p>`) | ||||
| 	test("d27ace93", `<p>d27ace93</p>`) | ||||
| 	test(sha[:14]+".x", `<p>`+sha[:14]+`.x</p>`) | ||||
|  | ||||
| 	expected14 := `<a href="` + commit[:len(commit)-(40-14)] + `" rel="nofollow"><code>` + sha[:10] + `</code></a>` | ||||
| 	expected14 := `<a href="` + commitPath[:len(commitPath)-(40-14)] + `" rel="nofollow"><code>` + sha[:10] + `</code></a>` | ||||
| 	test(sha[:14]+".", `<p>`+expected14+`.</p>`) | ||||
| 	test(sha[:14]+",", `<p>`+expected14+`,</p>`) | ||||
| 	test("["+sha[:14]+"]", `<p>[`+expected14+`]</p>`) | ||||
| @@ -80,10 +81,10 @@ func TestRender_CrossReferences(t *testing.T) { | ||||
|  | ||||
| 	test( | ||||
| 		"test-owner/test-repo#12345", | ||||
| 		`<p><a href="`+util.URLJoin(markup.TestAppURL, "test-owner", "test-repo", "issues", "12345")+`" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`) | ||||
| 		`<p><a href="/test-owner/test-repo/issues/12345" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`) | ||||
| 	test( | ||||
| 		"go-gitea/gitea#12345", | ||||
| 		`<p><a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`) | ||||
| 		`<p><a href="/go-gitea/gitea/issues/12345" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`) | ||||
| 	test( | ||||
| 		"/home/gitea/go-gitea/gitea#12345", | ||||
| 		`<p>/home/gitea/go-gitea/gitea#12345</p>`) | ||||
| @@ -487,7 +488,7 @@ func TestPostProcess_RenderDocument(t *testing.T) { | ||||
| 	// But cross-referenced issue index should work. | ||||
| 	test( | ||||
| 		"go-gitea/gitea#12345", | ||||
| 		`<a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue">go-gitea/gitea#12345</a>`) | ||||
| 		`<a href="/go-gitea/gitea/issues/12345" class="ref-issue">go-gitea/gitea#12345</a>`) | ||||
|  | ||||
| 	// Test that other post processing still works. | ||||
| 	test( | ||||
| @@ -543,7 +544,7 @@ func TestIssue18471(t *testing.T) { | ||||
| 	err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) | ||||
|  | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String()) | ||||
| 	assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code>783b039...da951ce</code></a>`, res.String()) | ||||
| } | ||||
|  | ||||
| func TestIsFullURL(t *testing.T) { | ||||
|   | ||||
| @@ -65,10 +65,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa | ||||
| 			g.transformHeading(ctx, v, reader, &tocList) | ||||
| 		case *ast.Paragraph: | ||||
| 			g.applyElementDir(v) | ||||
| 		case *ast.Image: | ||||
| 			g.transformImage(ctx, v) | ||||
| 		case *ast.Link: | ||||
| 			g.transformLink(ctx, v) | ||||
| 		case *ast.List: | ||||
| 			g.transformList(ctx, v, rc) | ||||
| 		case *ast.Text: | ||||
|   | ||||
| @@ -308,12 +308,12 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) { | ||||
| 	testcase := ` | ||||
|  | ||||
| ` | ||||
| 	expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a> | ||||
| <a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p> | ||||
| 	expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"/></a> | ||||
| <a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"/></a></p> | ||||
| ` | ||||
| 	res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase) | ||||
| 	res, err := markdown.RenderString(markup.NewTestRenderContext(), testcase) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, expected, res) | ||||
| 	assert.Equal(t, expected, string(res)) | ||||
| } | ||||
|  | ||||
| func TestRenderEmojiInLinks_Issue12331(t *testing.T) { | ||||
| @@ -529,3 +529,16 @@ space</p> | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, expected, string(result)) | ||||
| } | ||||
|  | ||||
| func TestMarkdownLink(t *testing.T) { | ||||
| 	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() | ||||
| 	input := `<a href=foo>link1</a> | ||||
| <a href='/foo'>link2</a> | ||||
| <a href="#foo">link3</a>` | ||||
| 	result, err := markdown.RenderString(markup.NewTestRenderContext("/base", localMetas), input) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, `<p><a href="/base/foo" rel="nofollow">link1</a> | ||||
| <a href="/base/foo" rel="nofollow">link2</a> | ||||
| <a href="#user-content-foo" rel="nofollow">link3</a></p> | ||||
| `, string(result)) | ||||
| } | ||||
|   | ||||
| @@ -1,59 +0,0 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package markdown | ||||
|  | ||||
| import ( | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
|  | ||||
| 	"github.com/yuin/goldmark/ast" | ||||
| ) | ||||
|  | ||||
| func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image) { | ||||
| 	// Images need two things: | ||||
| 	// | ||||
| 	// 1. Their src needs to munged to be a real value | ||||
| 	// 2. If they're not wrapped with a link they need a link wrapper | ||||
|  | ||||
| 	// Check if the destination is a real link | ||||
| 	if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) { | ||||
| 		v.Destination = []byte(ctx.RenderHelper.ResolveLink(string(v.Destination), markup.LinkTypeMedia)) | ||||
| 	} | ||||
|  | ||||
| 	parent := v.Parent() | ||||
| 	// Create a link around image only if parent is not already a link | ||||
| 	if _, ok := parent.(*ast.Link); !ok && parent != nil { | ||||
| 		next := v.NextSibling() | ||||
|  | ||||
| 		// Create a link wrapper | ||||
| 		wrap := ast.NewLink() | ||||
| 		wrap.Destination = v.Destination | ||||
| 		wrap.Title = v.Title | ||||
| 		wrap.SetAttributeString("target", []byte("_blank")) | ||||
|  | ||||
| 		// Duplicate the current image node | ||||
| 		image := ast.NewImage(ast.NewLink()) | ||||
| 		image.Destination = v.Destination | ||||
| 		image.Title = v.Title | ||||
| 		for _, attr := range v.Attributes() { | ||||
| 			image.SetAttribute(attr.Name, attr.Value) | ||||
| 		} | ||||
| 		for child := v.FirstChild(); child != nil; { | ||||
| 			next := child.NextSibling() | ||||
| 			image.AppendChild(image, child) | ||||
| 			child = next | ||||
| 		} | ||||
|  | ||||
| 		// Append our duplicate image to the wrapper link | ||||
| 		wrap.AppendChild(wrap, image) | ||||
|  | ||||
| 		// Wire in the next sibling | ||||
| 		wrap.SetNextSibling(next) | ||||
|  | ||||
| 		// Replace the current node with the wrapper link | ||||
| 		parent.ReplaceChild(parent, v, wrap) | ||||
|  | ||||
| 		// But most importantly ensure the next sibling is still on the old image too | ||||
| 		v.SetNextSibling(next) | ||||
| 	} | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package markdown | ||||
|  | ||||
| import ( | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
|  | ||||
| 	"github.com/yuin/goldmark/ast" | ||||
| ) | ||||
|  | ||||
| func resolveLink(ctx *markup.RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) { | ||||
| 	isAnchorFragment := link != "" && link[0] == '#' | ||||
| 	if !isAnchorFragment && !markup.IsFullURLString(link) { | ||||
| 		link, resolved = ctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault), true | ||||
| 	} | ||||
| 	if isAnchorFragment && userContentAnchorPrefix != "" { | ||||
| 		link, resolved = userContentAnchorPrefix+link[1:], true | ||||
| 	} | ||||
| 	return link, resolved | ||||
| } | ||||
|  | ||||
| func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) { | ||||
| 	if link, resolved := resolveLink(ctx, string(v.Destination), "#user-content-"); resolved { | ||||
| 		v.Destination = []byte(link) | ||||
| 	} | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package markup | ||||
| package orgmode | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| @@ -125,27 +125,13 @@ type orgWriter struct { | ||||
|  | ||||
| var _ org.Writer = (*orgWriter)(nil) | ||||
|  | ||||
| func (r *orgWriter) resolveLink(kind, link string) string { | ||||
| 	link = strings.TrimPrefix(link, "file:") | ||||
| 	if !strings.HasPrefix(link, "#") && // not a URL fragment | ||||
| 		!markup.IsFullURLString(link) { | ||||
| 		if kind == "regular" { | ||||
| 			// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]" | ||||
| 			// so we need to try to guess the link kind again here | ||||
| 			kind = org.RegularLink{URL: link}.Kind() | ||||
| 		} | ||||
| 		if kind == "image" || kind == "video" { | ||||
| 			link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeMedia) | ||||
| 		} else { | ||||
| 			link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault) | ||||
| 		} | ||||
| 	} | ||||
| 	return link | ||||
| func (r *orgWriter) resolveLink(link string) string { | ||||
| 	return strings.TrimPrefix(link, "file:") | ||||
| } | ||||
|  | ||||
| // WriteRegularLink renders images, links or videos | ||||
| func (r *orgWriter) WriteRegularLink(l org.RegularLink) { | ||||
| 	link := r.resolveLink(l.Kind(), l.URL) | ||||
| 	link := r.resolveLink(l.URL) | ||||
|  | ||||
| 	printHTML := func(html template.HTML, a ...any) { | ||||
| 		_, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...)) | ||||
| @@ -156,14 +142,14 @@ func (r *orgWriter) WriteRegularLink(l org.RegularLink) { | ||||
| 		if l.Description == nil { | ||||
| 			printHTML(`<img src="%s" alt="%s">`, link, link) | ||||
| 		} else { | ||||
| 			imageSrc := r.resolveLink(l.Kind(), org.String(l.Description...)) | ||||
| 			imageSrc := r.resolveLink(org.String(l.Description...)) | ||||
| 			printHTML(`<a href="%s"><img src="%s" alt="%s"></a>`, link, imageSrc, imageSrc) | ||||
| 		} | ||||
| 	case "video": | ||||
| 		if l.Description == nil { | ||||
| 			printHTML(`<video src="%s">%s</video>`, link, link) | ||||
| 		} else { | ||||
| 			videoSrc := r.resolveLink(l.Kind(), org.String(l.Description...)) | ||||
| 			videoSrc := r.resolveLink(org.String(l.Description...)) | ||||
| 			printHTML(`<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc) | ||||
| 		} | ||||
| 	default: | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package markup | ||||
| package orgmode_test | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| @@ -9,6 +9,7 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/orgmode" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @@ -22,7 +23,7 @@ func TestMain(m *testing.M) { | ||||
|  | ||||
| func TestRender_StandardLinks(t *testing.T) { | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/media/branch/main/"), input) | ||||
| 		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| 	} | ||||
| @@ -30,37 +31,37 @@ func TestRender_StandardLinks(t *testing.T) { | ||||
| 	test("[[https://google.com/]]", | ||||
| 		`<p><a href="https://google.com/">https://google.com/</a></p>`) | ||||
| 	test("[[ImageLink.svg][The Image Desc]]", | ||||
| 		`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`) | ||||
| 		`<p><a href="ImageLink.svg">The Image Desc</a></p>`) | ||||
| } | ||||
|  | ||||
| func TestRender_InternalLinks(t *testing.T) { | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/src/branch/main"), input) | ||||
| 		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| 	} | ||||
|  | ||||
| 	test("[[file:test.org][Test]]", | ||||
| 		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`) | ||||
| 		`<p><a href="test.org">Test</a></p>`) | ||||
| 	test("[[./test.org][Test]]", | ||||
| 		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`) | ||||
| 		`<p><a href="./test.org">Test</a></p>`) | ||||
| 	test("[[test.org][Test]]", | ||||
| 		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`) | ||||
| 		`<p><a href="test.org">Test</a></p>`) | ||||
| 	test("[[path/to/test.org][Test]]", | ||||
| 		`<p><a href="/relative-path/src/branch/main/path/to/test.org">Test</a></p>`) | ||||
| 		`<p><a href="path/to/test.org">Test</a></p>`) | ||||
| } | ||||
|  | ||||
| func TestRender_Media(t *testing.T) { | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(markup.NewTestRenderContext("./relative-path"), input) | ||||
| 		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| 	} | ||||
|  | ||||
| 	test("[[file:../../.images/src/02/train.jpg]]", | ||||
| 		`<p><img src=".images/src/02/train.jpg" alt=".images/src/02/train.jpg"></p>`) | ||||
| 		`<p><img src="../../.images/src/02/train.jpg" alt="../../.images/src/02/train.jpg"></p>`) | ||||
| 	test("[[file:train.jpg]]", | ||||
| 		`<p><img src="relative-path/train.jpg" alt="relative-path/train.jpg"></p>`) | ||||
| 		`<p><img src="train.jpg" alt="train.jpg"></p>`) | ||||
|  | ||||
| 	// With description. | ||||
| 	test("[[https://example.com][https://example.com/example.svg]]", | ||||
| @@ -91,7 +92,7 @@ func TestRender_Media(t *testing.T) { | ||||
|  | ||||
| func TestRender_Source(t *testing.T) { | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(markup.NewTestRenderContext(), input) | ||||
| 		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||
| 	} | ||||
|   | ||||
| @@ -261,9 +261,15 @@ func (r *TestRenderHelper) IsCommitIDExisting(commitID string) bool { | ||||
| 	return strings.HasPrefix(commitID, "65f1bf2") //|| strings.HasPrefix(commitID, "88fc37a") | ||||
| } | ||||
|  | ||||
| func (r *TestRenderHelper) ResolveLink(link string, likeType LinkType) string { | ||||
| func (r *TestRenderHelper) ResolveLink(link, preferLinkType string) string { | ||||
| 	linkType, link := ParseRenderedLink(link, preferLinkType) | ||||
| 	switch linkType { | ||||
| 	case LinkTypeRoot: | ||||
| 		return r.ctx.ResolveLinkRoot(link) | ||||
| 	default: | ||||
| 		return r.ctx.ResolveLinkRelative(r.BaseLink, "", link) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var _ RenderHelper = (*TestRenderHelper)(nil) | ||||
|  | ||||
|   | ||||
| @@ -10,13 +10,11 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
|  | ||||
| type LinkType string | ||||
|  | ||||
| const ( | ||||
| 	LinkTypeApp     LinkType = "app"     // the link is relative to the AppSubURL | ||||
| 	LinkTypeDefault LinkType = "default" // the link is relative to the default base (eg: repo link, or current ref tree path) | ||||
| 	LinkTypeMedia   LinkType = "media"   // the link should be used to access media files (images, videos) | ||||
| 	LinkTypeRaw     LinkType = "raw"     // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders | ||||
| 	LinkTypeDefault = "" | ||||
| 	LinkTypeRoot    = "/:root"  // the link is relative to the AppSubURL(ROOT_URL) | ||||
| 	LinkTypeMedia   = "/:media" // the link should be used to access media files (images, videos) | ||||
| 	LinkTypeRaw     = "/:raw"   // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders | ||||
| ) | ||||
|  | ||||
| type RenderHelper interface { | ||||
| @@ -27,7 +25,7 @@ type RenderHelper interface { | ||||
| 	// but not make processors to guess "is it rendering a comment or a wiki?" or "does it need to check commit ID?" | ||||
|  | ||||
| 	IsCommitIDExisting(commitID string) bool | ||||
| 	ResolveLink(link string, likeType LinkType) string | ||||
| 	ResolveLink(link, preferLinkType string) string | ||||
| } | ||||
|  | ||||
| // RenderHelperFuncs is used to decouple cycle-import | ||||
| @@ -51,7 +49,8 @@ func (r *SimpleRenderHelper) IsCommitIDExisting(commitID string) bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (r *SimpleRenderHelper) ResolveLink(link string, likeType LinkType) string { | ||||
| func (r *SimpleRenderHelper) ResolveLink(link, preferLinkType string) string { | ||||
| 	_, link = ParseRenderedLink(link, preferLinkType) | ||||
| 	return resolveLinkRelative(context.Background(), setting.AppSubURL+"/", "", link, false) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,10 +33,24 @@ func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute b | ||||
| 	return finalLink | ||||
| } | ||||
|  | ||||
| func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) (finalLink string) { | ||||
| func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) string { | ||||
| 	if strings.HasPrefix(link, "/:") { | ||||
| 		setting.PanicInDevOrTesting("invalid link %q, forgot to cut?", link) | ||||
| 	} | ||||
| 	return resolveLinkRelative(ctx, base, cur, link, ctx.RenderOptions.UseAbsoluteLink) | ||||
| } | ||||
|  | ||||
| func (ctx *RenderContext) ResolveLinkApp(link string) string { | ||||
| func (ctx *RenderContext) ResolveLinkRoot(link string) string { | ||||
| 	return ctx.ResolveLinkRelative(setting.AppSubURL+"/", "", link) | ||||
| } | ||||
|  | ||||
| func ParseRenderedLink(s, preferLinkType string) (linkType, link string) { | ||||
| 	if strings.HasPrefix(s, "/:") { | ||||
| 		p := strings.IndexByte(s[1:], '/') | ||||
| 		if p == -1 { | ||||
| 			return s, "" | ||||
| 		} | ||||
| 		return s[:p+1], s[p+2:] | ||||
| 	} | ||||
| 	return preferLinkType, s | ||||
| } | ||||
|   | ||||
| @@ -123,9 +123,9 @@ func TestRenderCommitBody(t *testing.T) { | ||||
|  | ||||
| [[local image|image.jpg]] | ||||
| [[remote link|<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>]] | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code class="nohighlight">88fc37a3c0...12fc37a3c0 (hash)</code></a> | ||||
| <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code class="nohighlight">88fc37a3c0</code></a> | ||||
| <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code>88fc37a3c0</code></a> | ||||
| com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit | ||||
| <span class="emoji" aria-label="thumbs up">👍</span> | ||||
| <a href="mailto:mail@domain.com">mail@domain.com</a> | ||||
|   | ||||
| @@ -134,7 +134,7 @@ Here are some links to the most important topics. You can find the full list of | ||||
| <h2 id="user-content-quick-links">Quick Links</h2> | ||||
| <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p> | ||||
| <p><a href="http://localhost:3000/user2/repo1/wiki/Configuration" rel="nofollow">Configuration</a> | ||||
| <a href="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> | ||||
| <a href="http://localhost:3000/user2/repo1/wiki/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> | ||||
| `, | ||||
| 	} | ||||
|  | ||||
| @@ -158,19 +158,19 @@ Here are some links to the most important topics. You can find the full list of | ||||
|  | ||||
| 	input := "[Link](test.md)\n" | ||||
| 	testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> | ||||
| <a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> | ||||
| <a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> | ||||
| `, http.StatusOK) | ||||
|  | ||||
| 	testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> | ||||
| <a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> | ||||
| <a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> | ||||
| `, http.StatusOK) | ||||
|  | ||||
| 	testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> | ||||
| <a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> | ||||
| <a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> | ||||
| `, http.StatusOK) | ||||
|  | ||||
| 	testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/path/test.md" rel="nofollow">Link</a> | ||||
| <a href="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p> | ||||
| <a href="http://localhost:3000/user2/repo1/src/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p> | ||||
| `, http.StatusOK) | ||||
|  | ||||
| 	testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 wxiaoguang
					wxiaoguang