mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +01:00 
			
		
		
		
	Support relative paths to videos from Wiki pages (#31061)
This change fixes cases when a Wiki page refers to a video stored in the Wiki repository using relative path. It follows the similar case which has been already implemented for images. Test plan: - Create repository and Wiki page - Clone the Wiki repository - Add video to it, say `video.mp4` - Modify the markdown file to refer to the video using `<video src="video.mp4">` - Commit the Wiki page - Observe that the video is properly displayed --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -88,6 +88,10 @@ func IsFullURLString(link string) bool { | |||||||
| 	return fullURLPattern.MatchString(link) | 	return fullURLPattern.MatchString(link) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func IsNonEmptyRelativePath(link string) bool { | ||||||
|  | 	return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#' | ||||||
|  | } | ||||||
|  |  | ||||||
| // regexp for full links to issues/pulls | // regexp for full links to issues/pulls | ||||||
| var issueFullPattern *regexp.Regexp | var issueFullPattern *regexp.Regexp | ||||||
|  |  | ||||||
| @@ -358,41 +362,6 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func handleNodeImg(ctx *RenderContext, img *html.Node) { |  | ||||||
| 	for i, attr := range img.Attr { |  | ||||||
| 		if attr.Key != "src" { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if attr.Val != "" && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "/") { |  | ||||||
| 			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val) |  | ||||||
|  |  | ||||||
| 			// 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 { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			if !hasParentAnchor { |  | ||||||
| 				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{ |  | ||||||
| 					{Key: "href", Val: attr.Val}, |  | ||||||
| 					{Key: "target", Val: "_blank"}, |  | ||||||
| 				}} |  | ||||||
| 				parent := img.Parent |  | ||||||
| 				imgNext := img.NextSibling |  | ||||||
| 				parent.RemoveChild(img) |  | ||||||
| 				parent.InsertBefore(imgA, imgNext) |  | ||||||
| 				imgA.AppendChild(img) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		attr.Val = camoHandleLink(attr.Val) |  | ||||||
| 		img.Attr[i] = attr |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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 | 	// Add user-content- to IDs and "#" links if they don't already have them | ||||||
| 	for idx, attr := range node.Attr { | 	for idx, attr := range node.Attr { | ||||||
| @@ -412,20 +381,20 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// We ignore code and pre. |  | ||||||
| 	switch node.Type { | 	switch node.Type { | ||||||
| 	case html.TextNode: | 	case html.TextNode: | ||||||
| 		processTextNodes(ctx, procs, node) | 		processTextNodes(ctx, procs, node) | ||||||
| 	case html.ElementNode: | 	case html.ElementNode: | ||||||
| 		if node.Data == "img" { | 		if node.Data == "code" || node.Data == "pre" { | ||||||
| 			next := node.NextSibling | 			// ignore code and pre nodes | ||||||
| 			handleNodeImg(ctx, node) | 			return node.NextSibling | ||||||
| 			return next | 		} else if node.Data == "img" { | ||||||
|  | 			return visitNodeImg(ctx, node) | ||||||
|  | 		} else if node.Data == "video" { | ||||||
|  | 			return visitNodeVideo(ctx, node) | ||||||
| 		} else if node.Data == "a" { | 		} else if node.Data == "a" { | ||||||
| 			// Restrict text in links to emojis | 			// Restrict text in links to emojis | ||||||
| 			procs = emojiProcessors | 			procs = emojiProcessors | ||||||
| 		} else if node.Data == "code" || node.Data == "pre" { |  | ||||||
| 			return node.NextSibling |  | ||||||
| 		} else if node.Data == "i" { | 		} else if node.Data == "i" { | ||||||
| 			for _, attr := range node.Attr { | 			for _, attr := range node.Attr { | ||||||
| 				if attr.Key != "class" { | 				if attr.Key != "class" { | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								modules/markup/html_node.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								modules/markup/html_node.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | // Copyright 2024 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package markup | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  |  | ||||||
|  | 	"golang.org/x/net/html" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { | ||||||
|  | 	next = img.NextSibling | ||||||
|  | 	for i, attr := range img.Attr { | ||||||
|  | 		if attr.Key != "src" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if IsNonEmptyRelativePath(attr.Val) { | ||||||
|  | 			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val) | ||||||
|  |  | ||||||
|  | 			// 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 { | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if !hasParentAnchor { | ||||||
|  | 				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{ | ||||||
|  | 					{Key: "href", Val: attr.Val}, | ||||||
|  | 					{Key: "target", Val: "_blank"}, | ||||||
|  | 				}} | ||||||
|  | 				parent := img.Parent | ||||||
|  | 				imgNext := img.NextSibling | ||||||
|  | 				parent.RemoveChild(img) | ||||||
|  | 				parent.InsertBefore(imgA, imgNext) | ||||||
|  | 				imgA.AppendChild(img) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		attr.Val = camoHandleLink(attr.Val) | ||||||
|  | 		img.Attr[i] = attr | ||||||
|  | 	} | ||||||
|  | 	return next | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) { | ||||||
|  | 	next = node.NextSibling | ||||||
|  | 	for i, attr := range node.Attr { | ||||||
|  | 		if attr.Key != "src" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if IsNonEmptyRelativePath(attr.Val) { | ||||||
|  | 			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val) | ||||||
|  | 		} | ||||||
|  | 		attr.Val = camoHandleLink(attr.Val) | ||||||
|  | 		node.Attr[i] = attr | ||||||
|  | 	} | ||||||
|  | 	return next | ||||||
|  | } | ||||||
| @@ -522,7 +522,7 @@ func TestRender_ShortLinks(t *testing.T) { | |||||||
| 		`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`) | 		`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRender_RelativeImages(t *testing.T) { | func TestRender_RelativeMedias(t *testing.T) { | ||||||
| 	render := func(input string, isWiki bool, links markup.Links) string { | 	render := func(input string, isWiki bool, links markup.Links) string { | ||||||
| 		buffer, err := markdown.RenderString(&markup.RenderContext{ | 		buffer, err := markdown.RenderString(&markup.RenderContext{ | ||||||
| 			Ctx:    git.DefaultContext, | 			Ctx:    git.DefaultContext, | ||||||
| @@ -548,6 +548,15 @@ func TestRender_RelativeImages(t *testing.T) { | |||||||
|  |  | ||||||
| 	out = render(`<img src="/LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"}) | 	out = render(`<img src="/LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"}) | ||||||
| 	assert.Equal(t, `<img src="/LINK"/>`, out) | 	assert.Equal(t, `<img src="/LINK"/>`, out) | ||||||
|  |  | ||||||
|  | 	out = render(`<video src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"}) | ||||||
|  | 	assert.Equal(t, `<video src="/test-owner/test-repo/LINK"></video>`, out) | ||||||
|  |  | ||||||
|  | 	out = render(`<video src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"}) | ||||||
|  | 	assert.Equal(t, `<video src="/test-owner/test-repo/wiki/raw/LINK"></video>`, out) | ||||||
|  |  | ||||||
|  | 	out = render(`<video src="/LINK">`, false, markup.Links{Base: "/test-owner/test-repo"}) | ||||||
|  | 	assert.Equal(t, `<video src="/LINK"></video>`, out) | ||||||
| } | } | ||||||
|  |  | ||||||
| func Test_ParseClusterFuzz(t *testing.T) { | func Test_ParseClusterFuzz(t *testing.T) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user