summaryrefslogtreecommitdiffstats
path: root/modules/markup
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-04-03 01:48:27 +0800
committerGitHub <noreply@github.com>2024-04-02 17:48:27 +0000
commitca5c895efb91d2c2f17a83460e1753101c6f6bb1 (patch)
tree351c56aa353b6147e335dbdb3892e513690ea0e8 /modules/markup
parenteb505b128c7b9b2459f2a5d20b5740017125178b (diff)
downloadgitea-ca5c895efb91d2c2f17a83460e1753101c6f6bb1.tar.gz
gitea-ca5c895efb91d2c2f17a83460e1753101c6f6bb1.zip
Render embedded code preview by permlink in markdown (#30234)
The permlink in markdown will be rendered as a code preview block, like GitHub Co-authored-by: silverwind <me@silverwind.io>
Diffstat (limited to 'modules/markup')
-rw-r--r--modules/markup/html.go1
-rw-r--r--modules/markup/html_codepreview.go92
-rw-r--r--modules/markup/html_codepreview_test.go34
-rw-r--r--modules/markup/renderer.go3
-rw-r--r--modules/markup/sanitizer.go15
5 files changed, 145 insertions, 0 deletions
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 21bd6206e0..56aa1cb49c 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
var defaultProcessors = []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
+ codePreviewPatternProcessor,
fullHashPatternProcessor,
shortLinkProcessor,
linkProcessor,
diff --git a/modules/markup/html_codepreview.go b/modules/markup/html_codepreview.go
new file mode 100644
index 0000000000..d9da24ea34
--- /dev/null
+++ b/modules/markup/html_codepreview.go
@@ -0,0 +1,92 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "html/template"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/httplib"
+ "code.gitea.io/gitea/modules/log"
+
+ "golang.org/x/net/html"
+)
+
+// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
+var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
+
+type RenderCodePreviewOptions struct {
+ FullURL string
+ OwnerName string
+ RepoName string
+ CommitID string
+ FilePath string
+
+ LineStart, LineStop int
+}
+
+func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
+ m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
+ if m == nil {
+ return 0, 0, "", nil
+ }
+
+ opts := RenderCodePreviewOptions{
+ FullURL: node.Data[m[0]:m[1]],
+ OwnerName: node.Data[m[2]:m[3]],
+ RepoName: node.Data[m[4]:m[5]],
+ CommitID: node.Data[m[6]:m[7]],
+ FilePath: node.Data[m[8]:m[9]],
+ }
+ if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
+ return 0, 0, "", nil
+ }
+ u, err := url.Parse(opts.FilePath)
+ if err != nil {
+ return 0, 0, "", err
+ }
+ opts.FilePath = strings.TrimPrefix(u.Path, "/")
+
+ lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
+ lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
+ lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
+ opts.LineStart, opts.LineStop = lineStart, lineStop
+ h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
+ return m[0], m[1], h, err
+}
+
+func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
+ for node != nil {
+ if node.Type != html.TextNode {
+ node = node.NextSibling
+ continue
+ }
+ urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
+ if err != nil || h == "" {
+ if err != nil {
+ log.Error("Unable to render code preview: %v", err)
+ }
+ node = node.NextSibling
+ continue
+ }
+ next := node.NextSibling
+ textBefore := node.Data[:urlPosStart]
+ textAfter := node.Data[urlPosEnd:]
+ // "textBefore" could be empty if there is only a URL in the text node, then an empty node (p, or li) will be left here.
+ // However, the empty node can't be simply removed, because:
+ // 1. the following processors will still try to access it (need to double-check undefined behaviors)
+ // 2. the new node is inserted as "<p>{TextBefore}<div NewNode/>{TextAfter}</p>" (the parent could also be "li")
+ // then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
+ // so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
+ node.Data = textBefore
+ node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
+ if textAfter != "" {
+ node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
+ }
+ node = next
+ }
+}
diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go
new file mode 100644
index 0000000000..d33630d040
--- /dev/null
+++ b/modules/markup/html_codepreview_test.go
@@ -0,0 +1,34 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup_test
+
+import (
+ "context"
+ "html/template"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderCodePreview(t *testing.T) {
+ markup.Init(&markup.ProcessorHelper{
+ RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+ return "<div>code preview</div>", nil
+ },
+ })
+ test := func(input, expected string) {
+ buffer, err := markup.RenderString(&markup.RenderContext{
+ Ctx: git.DefaultContext,
+ Type: "markdown",
+ }, input)
+ assert.NoError(t, err)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+ }
+ test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
+ test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
+}
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 0f0bf55740..005fcc278b 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
+ "html/template"
"io"
"net/url"
"path/filepath"
@@ -33,6 +34,8 @@ type ProcessorHelper struct {
IsUsernameMentionable func(ctx context.Context, username string) bool
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
+
+ RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
}
var DefaultProcessorHelper ProcessorHelper
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 79a2ba0dfb..77fbdf4520 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -60,6 +60,21 @@ func createDefaultPolicy() *bluemonday.Policy {
// For JS code copy and Mermaid loading state
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
+ // For code preview
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
+ policy.AllowAttrs("data-line-number").OnElements("span")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("code")
+
+ // For code preview (unicode escape)
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
+ policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
+
// For color preview
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")