mirror of https://github.com/go-gitea/gitea.git
Fix footnote jump behavior on the issue page. (#34621)
Close #34511 Close #34590 Add comment ID to the footnote item's id attribute to ensure uniqueness. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>pull/34602/head^2
parent
9165ea8713
commit
c6b2cbd75d
|
@ -5,6 +5,7 @@ package issues
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
|
@ -114,7 +115,9 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
|||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ type RepoCommentOptions struct {
|
|||
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
|
||||
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 {
|
||||
|
@ -53,10 +54,11 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor
|
|||
}
|
||||
rctx := markup.NewRenderContext(ctx)
|
||||
helper.ctx = rctx
|
||||
var metas map[string]string
|
||||
if repo != nil {
|
||||
helper.repoLink = repo.Link()
|
||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||
rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx))
|
||||
metas = repo.ComposeCommentMetas(ctx)
|
||||
} else {
|
||||
// this is almost dead code, only to pass the incorrect tests
|
||||
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",
|
||||
})
|
||||
}
|
||||
rctx = rctx.WithHelper(helper)
|
||||
metas["footnoteContextId"] = helper.opts.FootnoteContextID
|
||||
rctx = rctx.WithMetas(metas).WithHelper(helper)
|
||||
return rctx
|
||||
}
|
||||
|
|
|
@ -409,9 +409,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
|
|||
_, _ = w.Write(n.Name)
|
||||
_, _ = w.WriteString(`"><a href="#fn:`)
|
||||
_, _ = 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(`</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
|
||||
}
|
||||
|
|
|
@ -320,6 +320,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
|||
}
|
||||
|
||||
processNodeAttrID(node)
|
||||
processFootnoteNode(ctx, node) // FIXME: the footnote processing should be done in the "footnote.go" renderer directly
|
||||
|
||||
if isEmojiNode(node) {
|
||||
// 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{
|
||||
"user": "test-user", "repo": "test-repo",
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
"footnoteContextId": "12345",
|
||||
})
|
||||
out, err := markdown.RenderString(rctx, input)
|
||||
require.NoError(t, err)
|
||||
|
@ -69,4 +70,22 @@ func TestRender_IssueList(t *testing.T) {
|
|||
</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-")
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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
|
||||
|
@ -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) {
|
||||
for idx, attr := range node.Attr {
|
||||
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>
|
||||
</dl>
|
||||
<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>
|
||||
<hr/>
|
||||
<ol>
|
||||
|
|
|
@ -76,7 +76,11 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
|
|||
})
|
||||
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
||||
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)
|
||||
case "wiki":
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
|
@ -278,7 +279,9 @@ func UpdateCommentContent(ctx *context.Context) {
|
|||
|
||||
var renderedContent template.HTML
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -624,7 +625,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
|
|||
comment.Issue = issue
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
|
@ -700,7 +703,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
|
|||
}
|
||||
}
|
||||
} 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)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
|
@ -984,7 +989,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
|
|||
|
||||
func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -113,7 +114,9 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
|||
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)
|
||||
if err != nil {
|
||||
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-');
|
||||
|
||||
// scroll to anchor while respecting the `user-content` prefix that exists on the target
|
||||
function scrollToAnchor(encodedId: string): void {
|
||||
if (!encodedId) return;
|
||||
const id = decodeURIComponent(encodedId);
|
||||
const prefixedId = addPrefix(id);
|
||||
let el = document.querySelector(`#${prefixedId}`);
|
||||
function scrollToAnchor(encodedId?: string): void {
|
||||
// FIXME: need to rewrite this function with new a better markup anchor generation logic, too many tricks here
|
||||
let elemId: string;
|
||||
try {
|
||||
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]`
|
||||
if (!el) {
|
||||
el = document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`);
|
||||
}
|
||||
el = el ?? document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`);
|
||||
|
||||
// compat for links with old 'user-content-' prefixed hashes
|
||||
if (!el && hasPrefix(id)) {
|
||||
return document.querySelector(`#${id}`)?.scrollIntoView();
|
||||
}
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
el = (!el && hasPrefix(elemId)) ? document.getElementById(elemId) : el;
|
||||
|
||||
el?.scrollIntoView();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue