diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2024-04-03 01:48:27 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-02 17:48:27 +0000 |
commit | ca5c895efb91d2c2f17a83460e1753101c6f6bb1 (patch) | |
tree | 351c56aa353b6147e335dbdb3892e513690ea0e8 /modules/markup | |
parent | eb505b128c7b9b2459f2a5d20b5740017125178b (diff) | |
download | gitea-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.go | 1 | ||||
-rw-r--r-- | modules/markup/html_codepreview.go | 92 | ||||
-rw-r--r-- | modules/markup/html_codepreview_test.go | 34 | ||||
-rw-r--r-- | modules/markup/renderer.go | 3 | ||||
-rw-r--r-- | modules/markup/sanitizer.go | 15 |
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") |