mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Backport #34621 by @charles7668 Close #34511 Close #34590 Add comment ID to the footnote item's id attribute to ensure uniqueness. Co-authored-by: charles <30816317+charles7668@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -5,6 +5,7 @@ package issues | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/renderhelper" | 	"code.gitea.io/gitea/models/renderhelper" | ||||||
| @@ -114,7 +115,9 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var err error | 		var err error | ||||||
| 		rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) | 		rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ | ||||||
|  | 			FootnoteContextID: strconv.FormatInt(comment.ID, 10), | ||||||
|  | 		}) | ||||||
| 		if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil { | 		if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ type RepoCommentOptions struct { | |||||||
| 	DeprecatedRepoName  string // it is only a patch for the non-standard "markup" api | 	DeprecatedRepoName  string // it is only a patch for the non-standard "markup" api | ||||||
| 	DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api | 	DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api | ||||||
| 	CurrentRefPath      string // eg: "branch/main" or "commit/11223344" | 	CurrentRefPath      string // eg: "branch/main" or "commit/11223344" | ||||||
|  | 	FootnoteContextID   string // the extra context ID for footnotes, used to avoid conflicts with other footnotes in the same page | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext { | func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext { | ||||||
| @@ -53,10 +54,11 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor | |||||||
| 	} | 	} | ||||||
| 	rctx := markup.NewRenderContext(ctx) | 	rctx := markup.NewRenderContext(ctx) | ||||||
| 	helper.ctx = rctx | 	helper.ctx = rctx | ||||||
|  | 	var metas map[string]string | ||||||
| 	if repo != nil { | 	if repo != nil { | ||||||
| 		helper.repoLink = repo.Link() | 		helper.repoLink = repo.Link() | ||||||
| 		helper.commitChecker = newCommitChecker(ctx, repo) | 		helper.commitChecker = newCommitChecker(ctx, repo) | ||||||
| 		rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx)) | 		metas = repo.ComposeCommentMetas(ctx) | ||||||
| 	} else { | 	} else { | ||||||
| 		// this is almost dead code, only to pass the incorrect tests | 		// this is almost dead code, only to pass the incorrect tests | ||||||
| 		helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) | 		helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) | ||||||
| @@ -68,6 +70,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor | |||||||
| 			"markupAllowShortIssuePattern": "true", | 			"markupAllowShortIssuePattern": "true", | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 	rctx = rctx.WithHelper(helper) | 	metas["footnoteContextId"] = helper.opts.FootnoteContextID | ||||||
|  | 	rctx = rctx.WithMetas(metas).WithHelper(helper) | ||||||
| 	return rctx | 	return rctx | ||||||
| } | } | ||||||
|   | |||||||
| @@ -409,9 +409,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt | |||||||
| 		_, _ = w.Write(n.Name) | 		_, _ = w.Write(n.Name) | ||||||
| 		_, _ = w.WriteString(`"><a href="#fn:`) | 		_, _ = w.WriteString(`"><a href="#fn:`) | ||||||
| 		_, _ = w.Write(n.Name) | 		_, _ = w.Write(n.Name) | ||||||
| 		_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) | 		_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) // FIXME: here and below, need to keep the classes | ||||||
| 		_, _ = w.WriteString(is) | 		_, _ = w.WriteString(is) | ||||||
| 		_, _ = w.WriteString(`</a></sup>`) | 		_, _ = w.WriteString(` </a></sup>`) // the style doesn't work at the moment, so add a space to separate the names | ||||||
| 	} | 	} | ||||||
| 	return ast.WalkContinue, nil | 	return ast.WalkContinue, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -320,6 +320,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	processNodeAttrID(node) | 	processNodeAttrID(node) | ||||||
|  | 	processFootnoteNode(ctx, node) // FIXME: the footnote processing should be done in the "footnote.go" renderer directly | ||||||
|  |  | ||||||
| 	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" | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ func TestRender_IssueList(t *testing.T) { | |||||||
| 		rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{ | 		rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{ | ||||||
| 			"user": "test-user", "repo": "test-repo", | 			"user": "test-user", "repo": "test-repo", | ||||||
| 			"markupAllowShortIssuePattern": "true", | 			"markupAllowShortIssuePattern": "true", | ||||||
|  | 			"footnoteContextId":            "12345", | ||||||
| 		}) | 		}) | ||||||
| 		out, err := markdown.RenderString(rctx, input) | 		out, err := markdown.RenderString(rctx, input) | ||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| @@ -69,4 +70,22 @@ func TestRender_IssueList(t *testing.T) { | |||||||
| </ul>`, | </ul>`, | ||||||
| 		) | 		) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("IssueFootnote", func(t *testing.T) { | ||||||
|  | 		test( | ||||||
|  | 			"foo[^1][^2]\n\n[^1]: bar\n[^2]: baz", | ||||||
|  | 			`<p>foo<sup id="fnref:user-content-1-12345"><a href="#fn:user-content-1-12345" rel="nofollow">1 </a></sup><sup id="fnref:user-content-2-12345"><a href="#fn:user-content-2-12345" rel="nofollow">2 </a></sup></p> | ||||||
|  | <div> | ||||||
|  | <hr/> | ||||||
|  | <ol> | ||||||
|  | <li id="fn:user-content-1-12345"> | ||||||
|  | <p>bar <a href="#fnref:user-content-1-12345" rel="nofollow">↩︎</a></p> | ||||||
|  | </li> | ||||||
|  | <li id="fn:user-content-2-12345"> | ||||||
|  | <p>baz <a href="#fnref:user-content-2-12345" rel="nofollow">↩︎</a></p> | ||||||
|  | </li> | ||||||
|  | </ol> | ||||||
|  | </div>`, | ||||||
|  | 		) | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,6 +15,14 @@ func isAnchorIDUserContent(s string) bool { | |||||||
| 	return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") | 	return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func isAnchorIDFootnote(s string) bool { | ||||||
|  | 	return strings.HasPrefix(s, "fnref:user-content-") || strings.HasPrefix(s, "fn:user-content-") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isAnchorHrefFootnote(s string) bool { | ||||||
|  | 	return strings.HasPrefix(s, "#fnref:user-content-") || strings.HasPrefix(s, "#fn:user-content-") | ||||||
|  | } | ||||||
|  |  | ||||||
| func processNodeAttrID(node *html.Node) { | func processNodeAttrID(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, | ||||||
| 	// and convert the link href to a relative link to the host root | 	// and convert the link href to a relative link to the host root | ||||||
| @@ -27,6 +35,18 @@ func processNodeAttrID(node *html.Node) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func processFootnoteNode(ctx *RenderContext, node *html.Node) { | ||||||
|  | 	for idx, attr := range node.Attr { | ||||||
|  | 		if (attr.Key == "id" && isAnchorIDFootnote(attr.Val)) || | ||||||
|  | 			(attr.Key == "href" && isAnchorHrefFootnote(attr.Val)) { | ||||||
|  | 			if footnoteContextID := ctx.RenderOptions.Metas["footnoteContextId"]; footnoteContextID != "" { | ||||||
|  | 				node.Attr[idx].Val = attr.Val + "-" + footnoteContextID | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func processNodeA(ctx *RenderContext, node *html.Node) { | func processNodeA(ctx *RenderContext, node *html.Node) { | ||||||
| 	for idx, attr := range node.Attr { | 	for idx, attr := range node.Attr { | ||||||
| 		if attr.Key == "href" { | 		if attr.Key == "href" { | ||||||
|   | |||||||
| @@ -223,7 +223,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno | |||||||
| <dd>This is another definition of the second term.</dd> | <dd>This is another definition of the second term.</dd> | ||||||
| </dl> | </dl> | ||||||
| <h3 id="user-content-footnotes">Footnotes</h3> | <h3 id="user-content-footnotes">Footnotes</h3> | ||||||
| <p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p> | <p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1 </a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2 </a></sup></p> | ||||||
| <div> | <div> | ||||||
| <hr/> | <hr/> | ||||||
| <ol> | <ol> | ||||||
|   | |||||||
| @@ -76,7 +76,11 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur | |||||||
| 		}) | 		}) | ||||||
| 		rctx = rctx.WithMarkupType(markdown.MarkupName) | 		rctx = rctx.WithMarkupType(markdown.MarkupName) | ||||||
| 	case "comment": | 	case "comment": | ||||||
| 		rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) | 		rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{ | ||||||
|  | 			DeprecatedOwnerName: repoOwnerName, | ||||||
|  | 			DeprecatedRepoName:  repoName, | ||||||
|  | 			FootnoteContextID:   "preview", | ||||||
|  | 		}) | ||||||
| 		rctx = rctx.WithMarkupType(markdown.MarkupName) | 		rctx = rctx.WithMarkupType(markdown.MarkupName) | ||||||
| 	case "wiki": | 	case "wiki": | ||||||
| 		rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) | 		rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) | ||||||
|   | |||||||
| @@ -364,7 +364,9 @@ func UpdateIssueContent(ctx *context.Context) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) | 	rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{ | ||||||
|  | 		FootnoteContextID: "0", | ||||||
|  | 	}) | ||||||
| 	content, err := markdown.RenderString(rctx, issue.Content) | 	content, err := markdown.RenderString(rctx, issue.Content) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("RenderString", err) | 		ctx.ServerError("RenderString", err) | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
| 	"code.gitea.io/gitea/models/renderhelper" | 	"code.gitea.io/gitea/models/renderhelper" | ||||||
| @@ -278,7 +279,9 @@ func UpdateCommentContent(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	var renderedContent template.HTML | 	var renderedContent template.HTML | ||||||
| 	if comment.Content != "" { | 	if comment.Content != "" { | ||||||
| 		rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) | 		rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{ | ||||||
|  | 			FootnoteContextID: strconv.FormatInt(comment.ID, 10), | ||||||
|  | 		}) | ||||||
| 		renderedContent, err = markdown.RenderString(rctx, comment.Content) | 		renderedContent, err = markdown.RenderString(rctx, comment.Content) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.ServerError("RenderString", err) | 			ctx.ServerError("RenderString", err) | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"sort" | 	"sort" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
| 	activities_model "code.gitea.io/gitea/models/activities" | 	activities_model "code.gitea.io/gitea/models/activities" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| @@ -624,7 +625,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue | |||||||
| 		comment.Issue = issue | 		comment.Issue = issue | ||||||
|  |  | ||||||
| 		if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { | 		if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { | ||||||
| 			rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) | 			rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ | ||||||
|  | 				FootnoteContextID: strconv.FormatInt(comment.ID, 10), | ||||||
|  | 			}) | ||||||
| 			comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) | 			comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				ctx.ServerError("RenderString", err) | 				ctx.ServerError("RenderString", err) | ||||||
| @@ -700,7 +703,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} else if comment.Type.HasContentSupport() { | 		} else if comment.Type.HasContentSupport() { | ||||||
| 			rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) | 			rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ | ||||||
|  | 				FootnoteContextID: strconv.FormatInt(comment.ID, 10), | ||||||
|  | 			}) | ||||||
| 			comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) | 			comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				ctx.ServerError("RenderString", err) | 				ctx.ServerError("RenderString", err) | ||||||
| @@ -981,7 +986,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss | |||||||
|  |  | ||||||
| func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { | func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { | ||||||
| 	var err error | 	var err error | ||||||
| 	rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) | 	rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{ | ||||||
|  | 		FootnoteContextID: "0", // Set footnote context ID to 0 for the issue content | ||||||
|  | 	}) | ||||||
| 	issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content) | 	issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("RenderString", err) | 		ctx.ServerError("RenderString", err) | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| @@ -113,7 +114,9 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) | |||||||
| 			cacheUsers[r.PublisherID] = r.Publisher | 			cacheUsers[r.PublisherID] = r.Publisher | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo) | 		rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo, renderhelper.RepoCommentOptions{ | ||||||
|  | 			FootnoteContextID: strconv.FormatInt(r.ID, 10), | ||||||
|  | 		}) | ||||||
| 		r.RenderedNote, err = markdown.RenderString(rctx, r.Note) | 		r.RenderedNote, err = markdown.RenderString(rctx, r.Note) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
|   | |||||||
| @@ -5,21 +5,24 @@ const removePrefix = (str: string): string => str.replace(/^user-content-/, ''); | |||||||
| const hasPrefix = (str: string): boolean => str.startsWith('user-content-'); | const hasPrefix = (str: string): boolean => str.startsWith('user-content-'); | ||||||
|  |  | ||||||
| // scroll to anchor while respecting the `user-content` prefix that exists on the target | // scroll to anchor while respecting the `user-content` prefix that exists on the target | ||||||
| function scrollToAnchor(encodedId: string): void { | function scrollToAnchor(encodedId?: string): void { | ||||||
|   if (!encodedId) return; |   // FIXME: need to rewrite this function with new a better markup anchor generation logic, too many tricks here | ||||||
|   const id = decodeURIComponent(encodedId); |   let elemId: string; | ||||||
|   const prefixedId = addPrefix(id); |   try { | ||||||
|   let el = document.querySelector(`#${prefixedId}`); |     elemId = decodeURIComponent(encodedId ?? ''); | ||||||
|  |   } catch {} // ignore the errors, since the "encodedId" is from user's input | ||||||
|  |   if (!elemId) return; | ||||||
|  |  | ||||||
|  |   const prefixedId = addPrefix(elemId); | ||||||
|  |   // eslint-disable-next-line unicorn/prefer-query-selector | ||||||
|  |   let el = document.getElementById(prefixedId); | ||||||
|  |  | ||||||
|   // check for matching user-generated `a[name]` |   // check for matching user-generated `a[name]` | ||||||
|   if (!el) { |   el = el ?? document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`); | ||||||
|     el = document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // compat for links with old 'user-content-' prefixed hashes |   // compat for links with old 'user-content-' prefixed hashes | ||||||
|   if (!el && hasPrefix(id)) { |   // eslint-disable-next-line unicorn/prefer-query-selector | ||||||
|     return document.querySelector(`#${id}`)?.scrollIntoView(); |   el = (!el && hasPrefix(elemId)) ? document.getElementById(elemId) : el; | ||||||
|   } |  | ||||||
|  |  | ||||||
|   el?.scrollIntoView(); |   el?.scrollIntoView(); | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user