| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- // Copyright 2024 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package markup
-
- import (
- "strconv"
- "strings"
-
- "code.gitea.io/gitea/modules/httplib"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/references"
- "code.gitea.io/gitea/modules/regexplru"
- "code.gitea.io/gitea/modules/templates/vars"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/util"
-
- "golang.org/x/net/html"
- "golang.org/x/net/html/atom"
- )
-
- type RenderIssueIconTitleOptions struct {
- OwnerName string
- RepoName string
- LinkHref string
- IssueIndex int64
- }
-
- func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
- if ctx.RenderOptions.Metas == nil {
- return
- }
- next := node.NextSibling
- for node != nil && node != next {
- m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data)
- if m == nil {
- return
- }
-
- mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data)
- // leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files
- if mDiffView != nil {
- return
- }
-
- link := node.Data[m[0]:m[1]]
- if !httplib.IsCurrentGiteaSiteURL(ctx, link) {
- return
- }
- text := "#" + node.Data[m[2]:m[3]]
- // if m[4] and m[5] is not -1, then link is to a comment
- // indicate that in the text by appending (comment)
- if m[4] != -1 && m[5] != -1 {
- if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {
- text += " " + locale.TrString("repo.from_comment")
- } else {
- text += " (comment)"
- }
- }
-
- // extract repo and org name from matched link like
- // http://localhost:3000/gituser/myrepo/issues/1
- linkParts := strings.Split(link, "/")
- matchOrg := linkParts[len(linkParts)-4]
- matchRepo := linkParts[len(linkParts)-3]
-
- if matchOrg == ctx.RenderOptions.Metas["user"] && matchRepo == ctx.RenderOptions.Metas["repo"] {
- replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
- } else {
- text = matchOrg + "/" + matchRepo + text
- replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
- }
- node = node.NextSibling.NextSibling
- }
- }
-
- func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node {
- if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil {
- return nil
- }
- issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64)
- h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{
- OwnerName: ref.Owner,
- RepoName: ref.Name,
- LinkHref: ctx.RenderHelper.ResolveLink(linkHref, LinkTypeDefault),
- IssueIndex: issueIndex,
- })
- if err != nil {
- log.Error("RenderRepoIssueIconTitle failed: %v", err)
- return nil
- }
- if h == "" {
- return nil
- }
- return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))}
- }
-
- func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
- if ctx.RenderOptions.Metas == nil {
- return
- }
-
- // crossLinkOnly: do not parse "#123", only parse "owner/repo#123"
- // if there is no repo in the context, then the "#123" format can't be parsed
- // old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
- crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
-
- var ref *references.RenderizableReference
-
- next := node.NextSibling
- for node != nil && node != next {
- _, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
-
- // Repos with external issue trackers might still need to reference local PRs
- // We need to concern with the first one that shows up in the text, whichever it is
- isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
- refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
-
- switch ctx.RenderOptions.Metas["style"] {
- case "", IssueNameStyleNumeric:
- ref = refNumeric
- case IssueNameStyleAlphanumeric:
- ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
- case IssueNameStyleRegexp:
- pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
- if err != nil {
- return
- }
- ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
- }
-
- // Repos with external issue trackers might still need to reference local PRs
- // We need to concern with the first one that shows up in the text, whichever it is
- if hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
- // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
- // Allow a free-pass when non-numeric pattern wasn't found.
- if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
- ref = refNumeric
- }
- }
-
- if ref == nil {
- return
- }
-
- var link *html.Node
- refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
- if hasExtTrackFormat && !ref.IsPull {
- ctx.RenderOptions.Metas["index"] = ref.Issue
-
- res, err := vars.Expand(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas)
- if err != nil {
- // here we could just log the error and continue the rendering
- log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
- }
-
- link = createLink(ctx, res, refText, "ref-issue ref-external-issue")
- } else {
- // Path determines the type of link that will be rendered. It's unknown at this point whether
- // the linked item is actually a PR or an issue. Luckily it's of no real consequence because
- // Gitea will redirect on click as appropriate.
- issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner)
- issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name)
- issuePath := util.Iif(ref.IsPull, "pulls", "issues")
- 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
- // otherwise it would be too noisy for "take #1 as an example" in a sentence
- if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) {
- link = createIssueLinkContentWithSummary(ctx, linkHref, ref)
- }
- if link == nil {
- link = createLink(ctx, linkHref, refText, "ref-issue")
- }
- }
-
- if ref.Action == references.XRefActionNone {
- replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
- node = node.NextSibling.NextSibling
- continue
- }
-
- // Decorate action keywords if actionable
- var keyword *html.Node
- if references.IsXrefActionable(ref, hasExtTrackFormat) {
- keyword = createKeyword(ctx, node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
- } else {
- keyword = &html.Node{
- Type: html.TextNode,
- Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End],
- }
- }
- spaces := &html.Node{
- Type: html.TextNode,
- Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
- }
- replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link})
- node = node.NextSibling.NextSibling.NextSibling.NextSibling
- }
- }
|