mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01: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) | 	return r.commitChecker.IsCommitIDExisting(commitID) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) { | func (r *RepoComment) ResolveLink(link, preferLinkType string) string { | ||||||
| 	switch likeType { | 	linkType, link := markup.ParseRenderedLink(link, preferLinkType) | ||||||
| 	case markup.LinkTypeApp: | 	switch linkType { | ||||||
| 		finalLink = r.ctx.ResolveLinkApp(link) | 	case markup.LinkTypeRoot: | ||||||
|  | 		return r.ctx.ResolveLinkRoot(link) | ||||||
| 	default: | 	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) | var _ markup.RenderHelper = (*RepoComment)(nil) | ||||||
|   | |||||||
| @@ -29,17 +29,17 @@ func (r *RepoFile) IsCommitIDExisting(commitID string) bool { | |||||||
| 	return r.commitChecker.IsCommitIDExisting(commitID) | 	return r.commitChecker.IsCommitIDExisting(commitID) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string { | func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) { | ||||||
| 	finalLink := link | 	linkType, link := markup.ParseRenderedLink(link, preferLinkType) | ||||||
| 	switch likeType { | 	switch linkType { | ||||||
| 	case markup.LinkTypeApp: | 	case markup.LinkTypeRoot: | ||||||
| 		finalLink = r.ctx.ResolveLinkApp(link) | 		finalLink = r.ctx.ResolveLinkRoot(link) | ||||||
| 	case markup.LinkTypeDefault: |  | ||||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) |  | ||||||
| 	case markup.LinkTypeRaw: | 	case markup.LinkTypeRaw: | ||||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) | 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) | ||||||
| 	case markup.LinkTypeMedia: | 	case markup.LinkTypeMedia: | ||||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) | 		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 | 	return finalLink | ||||||
| } | } | ||||||
|   | |||||||
| @@ -48,8 +48,8 @@ func TestRepoFile(t *testing.T) { | |||||||
| 		assert.Equal(t, | 		assert.Equal(t, | ||||||
| 			`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a> | 			`<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/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/src/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></p> | ||||||
| `, rendered) | `, rendered) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @@ -62,7 +62,7 @@ func TestRepoFile(t *testing.T) { | |||||||
| `) | `) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a> | 		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) | `, rendered) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @@ -77,7 +77,7 @@ func TestRepoFile(t *testing.T) { | |||||||
| <video src="LINK"> | <video src="LINK"> | ||||||
| `) | `) | ||||||
| 		assert.NoError(t, err) | 		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 src="/user2/repo1/media/commit/1234/my-dir/LINK"> | ||||||
| </video>`, rendered) | </video>`, rendered) | ||||||
| 	}) | 	}) | ||||||
| @@ -100,7 +100,7 @@ func TestRepoFileOrgMode(t *testing.T) { | |||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, `<p> | 		assert.Equal(t, `<p> | ||||||
| <a href="https://google.com/" rel="nofollow">https://google.com/</a> | <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) | `, rendered) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,18 +30,16 @@ func (r *RepoWiki) IsCommitIDExisting(commitID string) bool { | |||||||
| 	return r.commitChecker.IsCommitIDExisting(commitID) | 	return r.commitChecker.IsCommitIDExisting(commitID) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string { | func (r *RepoWiki) ResolveLink(link, preferLinkType string) (finalLink string) { | ||||||
| 	finalLink := link | 	linkType, link := markup.ParseRenderedLink(link, preferLinkType) | ||||||
| 	switch likeType { | 	switch linkType { | ||||||
| 	case markup.LinkTypeApp: | 	case markup.LinkTypeRoot: | ||||||
| 		finalLink = r.ctx.ResolveLinkApp(link) | 		finalLink = r.ctx.ResolveLinkRoot(link) | ||||||
| 	case markup.LinkTypeDefault: | 	case markup.LinkTypeMedia, markup.LinkTypeRaw: | ||||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link) |  | ||||||
| 	case markup.LinkTypeMedia: |  | ||||||
| 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link) | 		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 | 	return finalLink | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,8 +45,8 @@ func TestRepoWiki(t *testing.T) { | |||||||
| 		assert.Equal(t, | 		assert.Equal(t, | ||||||
| 			`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a> | 			`<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/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/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></p> | ||||||
| `, rendered) | `, rendered) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @@ -57,7 +57,7 @@ func TestRepoWiki(t *testing.T) { | |||||||
| <video src="LINK"> | <video src="LINK"> | ||||||
| `) | `) | ||||||
| 		assert.NoError(t, err) | 		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 src="/user2/repo1/wiki/raw/LINK"> | ||||||
| </video>`, rendered) | </video>`, rendered) | ||||||
| 	}) | 	}) | ||||||
|   | |||||||
| @@ -15,9 +15,15 @@ type SimpleDocument struct { | |||||||
| 	baseLink string | 	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) | 		return r.ctx.ResolveLinkRelative(r.baseLink, "", link) | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| var _ markup.RenderHelper = (*SimpleDocument)(nil) | var _ markup.RenderHelper = (*SimpleDocument)(nil) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ func TestSimpleDocument(t *testing.T) { | |||||||
| 	assert.Equal(t, | 	assert.Equal(t, | ||||||
| 		`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d | 		`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d | ||||||
| #1 | #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> | <p><a href="/base/test" rel="nofollow">/test</a> | ||||||
| <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> | <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. | // 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 { | func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	var ( | 	baseLinkSrc := ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault) | ||||||
| 		command = strings.NewReplacer( | 	baseLinkRaw := ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw) | ||||||
| 			envMark("GITEA_PREFIX_SRC"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault), | 	command := strings.NewReplacer( | ||||||
| 			envMark("GITEA_PREFIX_RAW"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw), | 		envMark("GITEA_PREFIX_SRC"), baseLinkSrc, | ||||||
|  | 		envMark("GITEA_PREFIX_RAW"), baseLinkRaw, | ||||||
| 	).Replace(p.Command) | 	).Replace(p.Command) | ||||||
| 		commands = strings.Fields(command) | 	commands := strings.Fields(command) | ||||||
| 		args     = commands[1:] | 	args := commands[1:] | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	if p.IsInputFile { | 	if p.IsInputFile { | ||||||
| 		// write to temp file | 		// 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()) | 		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() | 	defer finished() | ||||||
|  |  | ||||||
| 	cmd := exec.CommandContext(processCtx, commands[0], args...) | 	cmd := exec.CommandContext(processCtx, commands[0], args...) | ||||||
| 	cmd.Env = append( | 	cmd.Env = append( | ||||||
| 		os.Environ(), | 		os.Environ(), | ||||||
| 		"GITEA_PREFIX_SRC="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault), | 		"GITEA_PREFIX_SRC="+baseLinkSrc, | ||||||
| 		"GITEA_PREFIX_RAW="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw), | 		"GITEA_PREFIX_RAW="+baseLinkRaw, | ||||||
| 	) | 	) | ||||||
| 	if !p.IsInputFile { | 	if !p.IsInputFile { | ||||||
| 		cmd.Stdin = input | 		cmd.Stdin = input | ||||||
|   | |||||||
| @@ -32,7 +32,6 @@ type globalVarsType struct { | |||||||
| 	comparePattern          *regexp.Regexp | 	comparePattern          *regexp.Regexp | ||||||
| 	fullURLPattern          *regexp.Regexp | 	fullURLPattern          *regexp.Regexp | ||||||
| 	emailRegex              *regexp.Regexp | 	emailRegex              *regexp.Regexp | ||||||
| 	blackfridayExtRegex     *regexp.Regexp |  | ||||||
| 	emojiShortCodeRegex     *regexp.Regexp | 	emojiShortCodeRegex     *regexp.Regexp | ||||||
| 	issueFullPattern        *regexp.Regexp | 	issueFullPattern        *regexp.Regexp | ||||||
| 	filesChangedFullPattern *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) | 	//   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|$))") | 	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: | 	// emojiShortCodeRegex find emoji by alias like :smile: | ||||||
| 	v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) | 	v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) | ||||||
|  |  | ||||||
| @@ -94,17 +90,12 @@ var globalVars = sync.OnceValue(func() *globalVarsType { | |||||||
| 	return v | 	return v | ||||||
| }) | }) | ||||||
|  |  | ||||||
| // IsFullURLBytes reports whether link fits valid format. |  | ||||||
| func IsFullURLBytes(link []byte) bool { |  | ||||||
| 	return globalVars().fullURLPattern.Match(link) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func IsFullURLString(link string) bool { | func IsFullURLString(link string) bool { | ||||||
| 	return globalVars().fullURLPattern.MatchString(link) | 	return globalVars().fullURLPattern.MatchString(link) | ||||||
| } | } | ||||||
|  |  | ||||||
| func IsNonEmptyRelativePath(link string) bool { | 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 | // 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 { | 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 | 	if node.Type == html.TextNode { | ||||||
| 	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: |  | ||||||
| 		for _, proc := range procs { | 		for _, proc := range procs { | ||||||
| 			proc(ctx, node) // it might add siblings | 			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) { | 	if isEmojiNode(node) { | ||||||
| 		// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span" | 		// 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 | 		// 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) | 		return visitNodeImg(ctx, node) | ||||||
| 	} else if node.Data == "video" { | 	} else if node.Data == "video" { | ||||||
| 		return visitNodeVideo(ctx, node) | 		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; { | 	for n := node.FirstChild; n != nil; { | ||||||
| 		n = visitNode(ctx, procs, n) | 		n = visitNode(ctx, procs, n) | ||||||
| 	} | 	} | ||||||
| 	default: |  | ||||||
| 	} |  | ||||||
| 	return node.NextSibling | 	return node.NextSibling | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -43,7 +43,6 @@ func createCodeLink(href, content, class string) *html.Node { | |||||||
| 	code := &html.Node{ | 	code := &html.Node{ | ||||||
| 		Type: html.ElementNode, | 		Type: html.ElementNode, | ||||||
| 		Data: atom.Code.String(), | 		Data: atom.Code.String(), | ||||||
| 		Attr: []html.Attribute{{Key: "class", Val: "nohighlight"}}, |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	code.AppendChild(text) | 	code.AppendChild(text) | ||||||
| @@ -189,7 +188,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 			continue | 			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")) | 		replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) | ||||||
| 		start = 0 | 		start = 0 | ||||||
| 		node = node.NextSibling.NextSibling | 		node = node.NextSibling.NextSibling | ||||||
| @@ -205,9 +204,9 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) | 		refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) | ||||||
| 		linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) | 		linkHref := "/:root/" + util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha) | ||||||
| 		link := createLink(ctx, linkHref, reftext, "commit") | 		link := createLink(ctx, linkHref, refText, "commit") | ||||||
|  |  | ||||||
| 		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) | 		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) | ||||||
| 		node = node.NextSibling.NextSibling | 		node = node.NextSibling.NextSibling | ||||||
|   | |||||||
| @@ -107,7 +107,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | |||||||
| 		isExternal := false | 		isExternal := false | ||||||
| 		if marker == "!" { | 		if marker == "!" { | ||||||
| 			path = "pulls" | 			path = "pulls" | ||||||
| 			prefix = "http://localhost:3000/someUser/someRepo/pulls/" | 			prefix = "/someUser/someRepo/pulls/" | ||||||
| 		} else { | 		} else { | ||||||
| 			path = "issues" | 			path = "issues" | ||||||
| 			prefix = "https://someurl.com/someUser/someRepo/" | 			prefix = "https://someurl.com/someUser/someRepo/" | ||||||
| @@ -116,7 +116,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | |||||||
|  |  | ||||||
| 		links := make([]any, len(indices)) | 		links := make([]any, len(indices)) | ||||||
| 		for i, index := range 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...) | 		expectedNil := fmt.Sprintf(expectedFmt, links...) | ||||||
| 		testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas)) | 		testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas)) | ||||||
| @@ -293,13 +293,13 @@ func TestRender_AutoLink(t *testing.T) { | |||||||
|  |  | ||||||
| 	// render valid commit URLs | 	// render valid commit URLs | ||||||
| 	tmp := util.URLJoin(TestRepoURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae") | 	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" | 	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 | 	// render other commit URLs | ||||||
| 	tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2" | 	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) { | func TestRender_FullIssueURLs(t *testing.T) { | ||||||
|   | |||||||
| @@ -82,7 +82,7 @@ func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref | |||||||
| 	h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{ | 	h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{ | ||||||
| 		OwnerName:  ref.Owner, | 		OwnerName:  ref.Owner, | ||||||
| 		RepoName:   ref.Name, | 		RepoName:   ref.Name, | ||||||
| 		LinkHref:   linkHref, | 		LinkHref:   ctx.RenderHelper.ResolveLink(linkHref, LinkTypeDefault), | ||||||
| 		IssueIndex: issueIndex, | 		IssueIndex: issueIndex, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	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) | 			issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) | ||||||
| 			issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) | 			issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) | ||||||
| 			issuePath := util.Iif(ref.IsPull, "pulls", "issues") | 			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 | 			// 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 | 			// 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) { | 	t.Run("NormalIssueRef", func(t *testing.T) { | ||||||
| 		test( | 		test( | ||||||
| 			"#12345", | 			"#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( | 		test( | ||||||
| 			"* foo #12345 bar", | 			"* foo #12345 bar", | ||||||
| 			`<ul> | 			`<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>`, | </ul>`, | ||||||
| 		) | 		) | ||||||
| 	}) | 	}) | ||||||
|   | |||||||
| @@ -125,7 +125,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if image { | 		if image { | ||||||
| 			link = ctx.RenderHelper.ResolveLink(link, LinkTypeMedia) |  | ||||||
| 			title := props["title"] | 			title := props["title"] | ||||||
| 			if title == "" { | 			if title == "" { | ||||||
| 				title = props["alt"] | 				title = props["alt"] | ||||||
| @@ -151,7 +150,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 				childNode.Attr = childNode.Attr[:2] | 				childNode.Attr = childNode.Attr[:2] | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			link = ctx.RenderHelper.ResolveLink(link, LinkTypeDefault) |  | ||||||
| 			childNode.Type = html.TextNode | 			childNode.Type = html.TextNode | ||||||
| 			childNode.Data = name | 			childNode.Data = name | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 		if ok && strings.Contains(mention, "/") { | 		if ok && strings.Contains(mention, "/") { | ||||||
| 			mentionOrgAndTeam := strings.Split(mention, "/") | 			mentionOrgAndTeam := strings.Split(mention, "/") | ||||||
| 			if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { | 			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*/)) | 				replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) | ||||||
| 				node = node.NextSibling.NextSibling | 				node = node.NextSibling.NextSibling | ||||||
| 				start = 0 | 				start = 0 | ||||||
| @@ -45,7 +45,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { | |||||||
| 		mentionedUsername := mention[1:] | 		mentionedUsername := mention[1:] | ||||||
|  |  | ||||||
| 		if DefaultRenderHelperFuncs != nil && DefaultRenderHelperFuncs.IsUsernameMentionable(ctx, mentionedUsername) { | 		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*/)) | 			replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) | ||||||
| 			node = node.NextSibling.NextSibling | 			node = node.NextSibling.NextSibling | ||||||
| 			start = 0 | 			start = 0 | ||||||
|   | |||||||
| @@ -4,42 +4,79 @@ | |||||||
| package markup | package markup | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"golang.org/x/net/html" | 	"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) { | func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { | ||||||
| 	next = img.NextSibling | 	next = img.NextSibling | ||||||
| 	for i, attr := range img.Attr { | 	for i, imgAttr := range img.Attr { | ||||||
| 		if attr.Key != "src" { | 		if imgAttr.Key != "src" { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if IsNonEmptyRelativePath(attr.Val) { | 		imgSrcOrigin := imgAttr.Val | ||||||
| 			attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia) | 		isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:") | ||||||
|  |  | ||||||
| 		// By default, the "<img>" tag should also be clickable, | 		// By default, the "<img>" tag should also be clickable, | ||||||
| 		// because frontend use `<img>` to paste the re-scaled image into the markdown, | 		// because frontend use `<img>` to paste the re-scaled image into the markdown, | ||||||
| 		// so it must match the default markdown image behavior. | 		// so it must match the default markdown image behavior. | ||||||
| 			hasParentAnchor := false | 		cnt := 0 | ||||||
| 			for p := img.Parent; p != nil; p = p.Parent { | 		for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent { | ||||||
| 				if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor { | 			if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor { | ||||||
|  | 				isLinkable = false | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
|  | 			cnt++ | ||||||
| 		} | 		} | ||||||
| 			if !hasParentAnchor { | 		if isLinkable { | ||||||
| 				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{ | 			wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{ | ||||||
| 					{Key: "href", Val: attr.Val}, | 				{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)}, | ||||||
| 				{Key: "target", Val: "_blank"}, | 				{Key: "target", Val: "_blank"}, | ||||||
| 			}} | 			}} | ||||||
| 			parent := img.Parent | 			parent := img.Parent | ||||||
| 			imgNext := img.NextSibling | 			imgNext := img.NextSibling | ||||||
| 			parent.RemoveChild(img) | 			parent.RemoveChild(img) | ||||||
| 				parent.InsertBefore(imgA, imgNext) | 			parent.InsertBefore(wrapper, imgNext) | ||||||
| 				imgA.AppendChild(img) | 			wrapper.AppendChild(img) | ||||||
| 		} | 		} | ||||||
| 		} |  | ||||||
| 		attr.Val = camoHandleLink(attr.Val) | 		imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia) | ||||||
| 		img.Attr[i] = attr | 		imgAttr.Val = camoHandleLink(imgAttr.Val) | ||||||
|  | 		img.Attr[i] = imgAttr | ||||||
| 	} | 	} | ||||||
| 	return next | 	return next | ||||||
| } | } | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ func TestRender_Commits(t *testing.T) { | |||||||
| 	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" | 	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" | ||||||
| 	repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/" | 	repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/" | ||||||
| 	commit := util.URLJoin(repo, "commit", sha) | 	commit := util.URLJoin(repo, "commit", sha) | ||||||
|  | 	commitPath := "/user13/repo11/commit/" + sha | ||||||
| 	tree := util.URLJoin(repo, "tree", sha, "src") | 	tree := util.URLJoin(repo, "tree", sha, "src") | ||||||
|  |  | ||||||
| 	file := util.URLJoin(repo, "commit", sha, "example.txt") | 	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) | 	commitCompare := util.URLJoin(repo, "compare", sha+"..."+sha) | ||||||
| 	commitCompareWithHash := commitCompare + "#L2" | 	commitCompareWithHash := commitCompare + "#L2" | ||||||
|  |  | ||||||
| 	test(sha, `<p><a href="`+commit+`" 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="`+commit[:len(commit)-(40-7)]+`" rel="nofollow"><code>65f1bf2</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="`+commit[:len(commit)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</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(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>`) | 	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(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(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("/home/gitea/"+sha, "<p>/home/gitea/"+sha+"</p>") | ||||||
| 	test("deadbeef", `<p>deadbeef</p>`) | 	test("deadbeef", `<p>deadbeef</p>`) | ||||||
| 	test("d27ace93", `<p>d27ace93</p>`) | 	test("d27ace93", `<p>d27ace93</p>`) | ||||||
| 	test(sha[:14]+".x", `<p>`+sha[:14]+`.x</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>`) | 	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( | ||||||
| 		"test-owner/test-repo#12345", | 		"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( | 	test( | ||||||
| 		"go-gitea/gitea#12345", | 		"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( | 	test( | ||||||
| 		"/home/gitea/go-gitea/gitea#12345", | 		"/home/gitea/go-gitea/gitea#12345", | ||||||
| 		`<p>/home/gitea/go-gitea/gitea#12345</p>`) | 		`<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. | 	// But cross-referenced issue index should work. | ||||||
| 	test( | 	test( | ||||||
| 		"go-gitea/gitea#12345", | 		"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 that other post processing still works. | ||||||
| 	test( | 	test( | ||||||
| @@ -543,7 +544,7 @@ func TestIssue18471(t *testing.T) { | |||||||
| 	err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) | 	err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) | ||||||
|  |  | ||||||
| 	assert.NoError(t, err) | 	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) { | 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) | 			g.transformHeading(ctx, v, reader, &tocList) | ||||||
| 		case *ast.Paragraph: | 		case *ast.Paragraph: | ||||||
| 			g.applyElementDir(v) | 			g.applyElementDir(v) | ||||||
| 		case *ast.Image: |  | ||||||
| 			g.transformImage(ctx, v) |  | ||||||
| 		case *ast.Link: |  | ||||||
| 			g.transformLink(ctx, v) |  | ||||||
| 		case *ast.List: | 		case *ast.List: | ||||||
| 			g.transformList(ctx, v, rc) | 			g.transformList(ctx, v, rc) | ||||||
| 		case *ast.Text: | 		case *ast.Text: | ||||||
|   | |||||||
| @@ -308,12 +308,12 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) { | |||||||
| 	testcase := ` | 	testcase := ` | ||||||
|  |  | ||||||
| ` | ` | ||||||
| 	expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a> | 	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> | <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.NoError(t, err) | ||||||
| 	assert.Equal(t, expected, res) | 	assert.Equal(t, expected, string(res)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRenderEmojiInLinks_Issue12331(t *testing.T) { | func TestRenderEmojiInLinks_Issue12331(t *testing.T) { | ||||||
| @@ -529,3 +529,16 @@ space</p> | |||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, expected, string(result)) | 	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. | // Copyright 2017 The Gitea Authors. All rights reserved. | ||||||
| // SPDX-License-Identifier: MIT | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
| package markup | package orgmode | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @@ -125,27 +125,13 @@ type orgWriter struct { | |||||||
|  |  | ||||||
| var _ org.Writer = (*orgWriter)(nil) | var _ org.Writer = (*orgWriter)(nil) | ||||||
|  |  | ||||||
| func (r *orgWriter) resolveLink(kind, link string) string { | func (r *orgWriter) resolveLink(link string) string { | ||||||
| 	link = strings.TrimPrefix(link, "file:") | 	return 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 |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // WriteRegularLink renders images, links or videos | // WriteRegularLink renders images, links or videos | ||||||
| func (r *orgWriter) WriteRegularLink(l org.RegularLink) { | 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) { | 	printHTML := func(html template.HTML, a ...any) { | ||||||
| 		_, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...)) | 		_, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...)) | ||||||
| @@ -156,14 +142,14 @@ func (r *orgWriter) WriteRegularLink(l org.RegularLink) { | |||||||
| 		if l.Description == nil { | 		if l.Description == nil { | ||||||
| 			printHTML(`<img src="%s" alt="%s">`, link, link) | 			printHTML(`<img src="%s" alt="%s">`, link, link) | ||||||
| 		} else { | 		} 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) | 			printHTML(`<a href="%s"><img src="%s" alt="%s"></a>`, link, imageSrc, imageSrc) | ||||||
| 		} | 		} | ||||||
| 	case "video": | 	case "video": | ||||||
| 		if l.Description == nil { | 		if l.Description == nil { | ||||||
| 			printHTML(`<video src="%s">%s</video>`, link, link) | 			printHTML(`<video src="%s">%s</video>`, link, link) | ||||||
| 		} else { | 		} 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) | 			printHTML(`<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc) | ||||||
| 		} | 		} | ||||||
| 	default: | 	default: | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | // Copyright 2017 The Gitea Authors. All rights reserved. | ||||||
| // SPDX-License-Identifier: MIT | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
| package markup | package orgmode_test | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"os" | 	"os" | ||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup/orgmode" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @@ -22,7 +23,7 @@ func TestMain(m *testing.M) { | |||||||
|  |  | ||||||
| func TestRender_StandardLinks(t *testing.T) { | func TestRender_StandardLinks(t *testing.T) { | ||||||
| 	test := func(input, expected string) { | 	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.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| @@ -30,37 +31,37 @@ func TestRender_StandardLinks(t *testing.T) { | |||||||
| 	test("[[https://google.com/]]", | 	test("[[https://google.com/]]", | ||||||
| 		`<p><a href="https://google.com/">https://google.com/</a></p>`) | 		`<p><a href="https://google.com/">https://google.com/</a></p>`) | ||||||
| 	test("[[ImageLink.svg][The Image Desc]]", | 	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) { | func TestRender_InternalLinks(t *testing.T) { | ||||||
| 	test := func(input, expected string) { | 	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.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	test("[[file:test.org][Test]]", | 	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]]", | 	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]]", | 	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]]", | 	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) { | func TestRender_Media(t *testing.T) { | ||||||
| 	test := func(input, expected string) { | 	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.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	test("[[file:../../.images/src/02/train.jpg]]", | 	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]]", | 	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. | 	// With description. | ||||||
| 	test("[[https://example.com][https://example.com/example.svg]]", | 	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) { | func TestRender_Source(t *testing.T) { | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer, err := RenderString(markup.NewTestRenderContext(), input) | 		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		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") | 	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) | 		return r.ctx.ResolveLinkRelative(r.BaseLink, "", link) | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| var _ RenderHelper = (*TestRenderHelper)(nil) | var _ RenderHelper = (*TestRenderHelper)(nil) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,13 +10,11 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type LinkType string |  | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	LinkTypeApp     LinkType = "app"     // the link is relative to the AppSubURL | 	LinkTypeDefault = "" | ||||||
| 	LinkTypeDefault LinkType = "default" // the link is relative to the default base (eg: repo link, or current ref tree path) | 	LinkTypeRoot    = "/:root"  // the link is relative to the AppSubURL(ROOT_URL) | ||||||
| 	LinkTypeMedia   LinkType = "media"   // the link should be used to access media files (images, videos) | 	LinkTypeMedia   = "/: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 | 	LinkTypeRaw     = "/:raw"   // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type RenderHelper interface { | 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?" | 	// 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 | 	IsCommitIDExisting(commitID string) bool | ||||||
| 	ResolveLink(link string, likeType LinkType) string | 	ResolveLink(link, preferLinkType string) string | ||||||
| } | } | ||||||
|  |  | ||||||
| // RenderHelperFuncs is used to decouple cycle-import | // RenderHelperFuncs is used to decouple cycle-import | ||||||
| @@ -51,7 +49,8 @@ func (r *SimpleRenderHelper) IsCommitIDExisting(commitID string) bool { | |||||||
| 	return false | 	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) | 	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 | 	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) | 	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) | 	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]] | [[local image|image.jpg]] | ||||||
| [[remote link|<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>]] | [[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 | 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 | com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit | ||||||
| <span class="emoji" aria-label="thumbs up">👍</span> | <span class="emoji" aria-label="thumbs up">👍</span> | ||||||
| <a href="mailto:mail@domain.com">mail@domain.com</a> | <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> | <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>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> | <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" | 	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> | 	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) | `, http.StatusOK) | ||||||
|  |  | ||||||
| 	testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> | 	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) | `, http.StatusOK) | ||||||
|  |  | ||||||
| 	testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> | 	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) | `, 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> | 	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) | `, http.StatusOK) | ||||||
|  |  | ||||||
| 	testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity) | 	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