Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor markup render system #32645

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 53 additions & 68 deletions modules/markup/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ package markup

import (
"bytes"
"fmt"
"io"
"regexp"
"slices"
"strings"
"sync"

Expand Down Expand Up @@ -133,75 +133,49 @@ func CustomLinkURLSchemes(schemes []string) {
common.GlobalVars().LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|"))
}

type postProcessError struct {
context string
err error
}

func (p *postProcessError) Error() string {
return "PostProcess: " + p.context + ", " + p.err.Error()
}

type processor func(ctx *RenderContext, node *html.Node)

var defaultProcessors = []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
codePreviewPatternProcessor,
fullHashPatternProcessor,
shortLinkProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emailAddressProcessor,
emojiProcessor,
emojiShortCodeProcessor,
}

// PostProcess does the final required transformations to the passed raw HTML
// PostProcessDefault does the final required transformations to the passed raw HTML
// data, and ensures its validity. Transformations include: replacing links and
// emails with HTML links, parsing shortlinks in the format of [[Link]], like
// MediaWiki, linking issues in the format #ID, and mentions in the format
// @user, and others.
func PostProcess(ctx *RenderContext, input io.Reader, output io.Writer) error {
return postProcess(ctx, defaultProcessors, input, output)
}

var commitMessageProcessors = []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emailAddressProcessor,
emojiProcessor,
emojiShortCodeProcessor,
func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) error {
procs := []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
codePreviewPatternProcessor,
fullHashPatternProcessor,
shortLinkProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emailAddressProcessor,
emojiProcessor,
emojiShortCodeProcessor,
}
return postProcess(ctx, procs, input, output)
}

// RenderCommitMessage will use the same logic as PostProcess, but will disable
// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
// set, which changes every text node into a link to the passed default link.
// the shortLinkProcessor.
func RenderCommitMessage(ctx *RenderContext, content string) (string, error) {
procs := commitMessageProcessors
return renderProcessString(ctx, procs, content)
}

var commitMessageSubjectProcessors = []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emojiShortCodeProcessor,
emojiProcessor,
procs := []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emailAddressProcessor,
emojiProcessor,
6543 marked this conversation as resolved.
Show resolved Hide resolved
emojiShortCodeProcessor,
}
return postProcessString(ctx, procs, content)
}

var emojiProcessors = []processor{
Expand All @@ -214,7 +188,18 @@ var emojiProcessors = []processor{
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
// which changes every text node into a link to the passed default link.
func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
procs := slices.Clone(commitMessageSubjectProcessors)
procs := []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emojiShortCodeProcessor,
emojiProcessor,
}
procs = append(procs, func(ctx *RenderContext, node *html.Node) {
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
node.Type = html.ElementNode
Expand All @@ -223,19 +208,19 @@ func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string)
node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}}
node.FirstChild, node.LastChild = ch, ch
})
return renderProcessString(ctx, procs, content)
return postProcessString(ctx, procs, content)
}

// RenderIssueTitle to process title on individual issue/pull page
func RenderIssueTitle(ctx *RenderContext, title string) (string, error) {
// do not render other issue/commit links in an issue's title - which in most cases is already a link.
return renderProcessString(ctx, []processor{
return postProcessString(ctx, []processor{
emojiShortCodeProcessor,
emojiProcessor,
}, title)
}

func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
var buf strings.Builder
if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
return "", err
Expand All @@ -246,7 +231,7 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string)
// RenderDescriptionHTML will use similar logic as PostProcess, but will
// use a single special linkProcessor.
func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
return renderProcessString(ctx, []processor{
return postProcessString(ctx, []processor{
descriptionLinkProcessor,
emojiShortCodeProcessor,
emojiProcessor,
Expand All @@ -256,7 +241,7 @@ func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
// RenderEmoji for when we want to just process emoji and shortcodes
// in various places it isn't already run through the normal markdown processor
func RenderEmoji(ctx *RenderContext, content string) (string, error) {
return renderProcessString(ctx, emojiProcessors, content)
return postProcessString(ctx, emojiProcessors, content)
}

func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
Expand All @@ -276,7 +261,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
strings.NewReader("</body></html>"),
))
if err != nil {
return &postProcessError{"invalid HTML", err}
return fmt.Errorf("markup.postProcess: invalid HTML: %w", err)
}

if node.Type == html.DocumentNode {
Expand Down Expand Up @@ -308,7 +293,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
// Render everything to buf.
for _, node := range newNodes {
if err := html.Render(output, node); err != nil {
return &postProcessError{"error rendering processed HTML", err}
return fmt.Errorf("markup.postProcess: html.Render: %w", err)
}
}
return nil
Expand Down
4 changes: 2 additions & 2 deletions modules/markup/html_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,12 @@ func TestRender_AutoLink(t *testing.T) {

test := func(input, expected string) {
var buffer strings.Builder
err := PostProcess(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))

buffer.Reset()
err = PostProcess(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
}
Expand Down
14 changes: 7 additions & 7 deletions modules/markup/html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,14 @@ func Test_ParseClusterFuzz(t *testing.T) {
data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "

var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html")

data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "

res.Reset()
err = markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
err = markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)

assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html")
Expand All @@ -464,7 +464,7 @@ func TestPostProcess_RenderDocument(t *testing.T) {

test := func(input, expected string) {
var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(markup.TestAppURL, map[string]string{"user": "go-gitea", "repo": "gitea"}), strings.NewReader(input), &res)
err := markup.PostProcessDefault(markup.NewTestRenderContext(markup.TestAppURL, map[string]string{"user": "go-gitea", "repo": "gitea"}), strings.NewReader(input), &res)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
}
Expand Down Expand Up @@ -501,7 +501,7 @@ func TestIssue16020(t *testing.T) {
data := `<img src="data:image/png;base64,i//V"/>`

var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err)
assert.Equal(t, data, res.String())
}
Expand All @@ -514,23 +514,23 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(b, err)
}
}

func TestFuzz(t *testing.T) {
s := "t/l/issues/8#/../../a"
renderContext := markup.NewTestRenderContext()
err := markup.PostProcess(renderContext, strings.NewReader(s), io.Discard)
err := markup.PostProcessDefault(renderContext, strings.NewReader(s), io.Discard)
assert.NoError(t, err)
}

func TestIssue18471(t *testing.T) {
data := `http://domain/org/repo/compare/783b039...da951ce`

var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)

assert.NoError(t, err)
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
Expand Down
4 changes: 1 addition & 3 deletions modules/markup/markdown/goldmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
// especially in many tests.
markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"]
if markup.RenderBehaviorForTesting.ForceHardLineBreak {
v.SetHardLineBreak(true)
} else if markdownLineBreakStyle == "comment" {
if markdownLineBreakStyle == "comment" {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
} else if markdownLineBreakStyle == "document" {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
Expand Down
Loading
Loading