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 (
|
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)
|
||||||
|
@ -984,7 +989,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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue