Skip to content

Commit 7170612

Browse files
authored
Refactor markdown render (#30139)
Only split the file into small ones (and rename AttentionTypes to attentionTypes)
1 parent 7fda109 commit 7170612

8 files changed

+364
-257
lines changed

modules/markup/markdown/goldmark.go

Lines changed: 7 additions & 256 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,14 @@
44
package markdown
55

66
import (
7-
"bytes"
87
"fmt"
98
"regexp"
109
"strings"
1110

1211
"code.gitea.io/gitea/modules/container"
1312
"code.gitea.io/gitea/modules/markup"
14-
"code.gitea.io/gitea/modules/markup/common"
1513
"code.gitea.io/gitea/modules/setting"
16-
"code.gitea.io/gitea/modules/svg"
17-
giteautil "code.gitea.io/gitea/modules/util"
1814

19-
"github.com/microcosm-cc/bluemonday/css"
2015
"github.com/yuin/goldmark/ast"
2116
east "github.com/yuin/goldmark/extension/ast"
2217
"github.com/yuin/goldmark/parser"
@@ -28,12 +23,12 @@ import (
2823

2924
// ASTTransformer is a default transformer of the goldmark tree.
3025
type ASTTransformer struct {
31-
AttentionTypes container.Set[string]
26+
attentionTypes container.Set[string]
3227
}
3328

3429
func NewASTTransformer() *ASTTransformer {
3530
return &ASTTransformer{
36-
AttentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
31+
attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
3732
}
3833
}
3934

@@ -66,123 +61,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
6661

6762
switch v := n.(type) {
6863
case *ast.Heading:
69-
for _, attr := range v.Attributes() {
70-
if _, ok := attr.Value.([]byte); !ok {
71-
v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
72-
}
73-
}
74-
txt := n.Text(reader.Source())
75-
header := markup.Header{
76-
Text: util.BytesToReadOnlyString(txt),
77-
Level: v.Level,
78-
}
79-
if id, found := v.AttributeString("id"); found {
80-
header.ID = util.BytesToReadOnlyString(id.([]byte))
81-
}
82-
tocList = append(tocList, header)
83-
g.applyElementDir(v)
64+
g.transformHeading(ctx, v, reader, &tocList)
8465
case *ast.Paragraph:
8566
g.applyElementDir(v)
8667
case *ast.Image:
87-
// Images need two things:
88-
//
89-
// 1. Their src needs to munged to be a real value
90-
// 2. If they're not wrapped with a link they need a link wrapper
91-
92-
// Check if the destination is a real link
93-
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
94-
v.Destination = []byte(giteautil.URLJoin(
95-
ctx.Links.ResolveMediaLink(ctx.IsWiki),
96-
strings.TrimLeft(string(v.Destination), "/"),
97-
))
98-
}
99-
100-
parent := n.Parent()
101-
// Create a link around image only if parent is not already a link
102-
if _, ok := parent.(*ast.Link); !ok && parent != nil {
103-
next := n.NextSibling()
104-
105-
// Create a link wrapper
106-
wrap := ast.NewLink()
107-
wrap.Destination = v.Destination
108-
wrap.Title = v.Title
109-
wrap.SetAttributeString("target", []byte("_blank"))
110-
111-
// Duplicate the current image node
112-
image := ast.NewImage(ast.NewLink())
113-
image.Destination = v.Destination
114-
image.Title = v.Title
115-
for _, attr := range v.Attributes() {
116-
image.SetAttribute(attr.Name, attr.Value)
117-
}
118-
for child := v.FirstChild(); child != nil; {
119-
next := child.NextSibling()
120-
image.AppendChild(image, child)
121-
child = next
122-
}
123-
124-
// Append our duplicate image to the wrapper link
125-
wrap.AppendChild(wrap, image)
126-
127-
// Wire in the next sibling
128-
wrap.SetNextSibling(next)
129-
130-
// Replace the current node with the wrapper link
131-
parent.ReplaceChild(parent, n, wrap)
132-
133-
// But most importantly ensure the next sibling is still on the old image too
134-
v.SetNextSibling(next)
135-
}
68+
g.transformImage(ctx, v, reader)
13669
case *ast.Link:
137-
// Links need their href to munged to be a real value
138-
link := v.Destination
139-
isAnchorFragment := len(link) > 0 && link[0] == '#'
140-
if !isAnchorFragment && !markup.IsFullURLBytes(link) {
141-
base := ctx.Links.Base
142-
if ctx.IsWiki {
143-
base = ctx.Links.WikiLink()
144-
} else if ctx.Links.HasBranchInfo() {
145-
base = ctx.Links.SrcLink()
146-
}
147-
link = []byte(giteautil.URLJoin(base, string(link)))
148-
}
149-
if isAnchorFragment {
150-
link = []byte("#user-content-" + string(link)[1:])
151-
}
152-
v.Destination = link
70+
g.transformLink(ctx, v, reader)
15371
case *ast.List:
154-
if v.HasChildren() {
155-
children := make([]ast.Node, 0, v.ChildCount())
156-
child := v.FirstChild()
157-
for child != nil {
158-
children = append(children, child)
159-
child = child.NextSibling()
160-
}
161-
v.RemoveChildren(v)
162-
163-
for _, child := range children {
164-
listItem := child.(*ast.ListItem)
165-
if !child.HasChildren() || !child.FirstChild().HasChildren() {
166-
v.AppendChild(v, child)
167-
continue
168-
}
169-
taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
170-
if !ok {
171-
v.AppendChild(v, child)
172-
continue
173-
}
174-
newChild := NewTaskCheckBoxListItem(listItem)
175-
newChild.IsChecked = taskCheckBox.IsChecked
176-
newChild.SetAttributeString("class", []byte("task-list-item"))
177-
segments := newChild.FirstChild().Lines()
178-
if segments.Len() > 0 {
179-
segment := segments.At(0)
180-
newChild.SourcePosition = rc.metaLength + segment.Start
181-
}
182-
v.AppendChild(v, newChild)
183-
}
184-
}
185-
g.applyElementDir(v)
72+
g.transformList(ctx, v, reader, rc)
18673
case *ast.Text:
18774
if v.SoftLineBreak() && !v.HardLineBreak() {
18875
if ctx.Metas["mode"] != "document" {
@@ -192,10 +79,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
19279
}
19380
}
19481
case *ast.CodeSpan:
195-
colorContent := n.Text(reader.Source())
196-
if css.ColorHandler(strings.ToLower(string(colorContent))) {
197-
v.AppendChild(v, NewColorPreview(colorContent))
198-
}
82+
g.transformCodeSpan(ctx, v, reader)
19983
case *ast.Blockquote:
20084
return g.transformBlockquote(v, reader)
20185
}
@@ -219,50 +103,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
219103
}
220104
}
221105

222-
type prefixedIDs struct {
223-
values container.Set[string]
224-
}
225-
226-
// Generate generates a new element id.
227-
func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
228-
dft := []byte("id")
229-
if kind == ast.KindHeading {
230-
dft = []byte("heading")
231-
}
232-
return p.GenerateWithDefault(value, dft)
233-
}
234-
235-
// GenerateWithDefault generates a new element id.
236-
func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
237-
result := common.CleanValue(value)
238-
if len(result) == 0 {
239-
result = dft
240-
}
241-
if !bytes.HasPrefix(result, []byte("user-content-")) {
242-
result = append([]byte("user-content-"), result...)
243-
}
244-
if p.values.Add(util.BytesToReadOnlyString(result)) {
245-
return result
246-
}
247-
for i := 1; ; i++ {
248-
newResult := fmt.Sprintf("%s-%d", result, i)
249-
if p.values.Add(newResult) {
250-
return []byte(newResult)
251-
}
252-
}
253-
}
254-
255-
// Put puts a given element id to the used ids table.
256-
func (p *prefixedIDs) Put(value []byte) {
257-
p.values.Add(util.BytesToReadOnlyString(value))
258-
}
259-
260-
func newPrefixedIDs() *prefixedIDs {
261-
return &prefixedIDs{
262-
values: make(container.Set[string]),
263-
}
264-
}
265-
266106
// NewHTMLRenderer creates a HTMLRenderer to render
267107
// in the gitea form.
268108
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
@@ -295,60 +135,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
295135
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
296136
}
297137

298-
// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
299-
// See #21474 for reference
300-
func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
301-
if entering {
302-
if n.Attributes() != nil {
303-
_, _ = w.WriteString("<code")
304-
html.RenderAttributes(w, n, html.CodeAttributeFilter)
305-
_ = w.WriteByte('>')
306-
} else {
307-
_, _ = w.WriteString("<code>")
308-
}
309-
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
310-
switch v := c.(type) {
311-
case *ast.Text:
312-
segment := v.Segment
313-
value := segment.Value(source)
314-
if bytes.HasSuffix(value, []byte("\n")) {
315-
r.Writer.RawWrite(w, value[:len(value)-1])
316-
r.Writer.RawWrite(w, []byte(" "))
317-
} else {
318-
r.Writer.RawWrite(w, value)
319-
}
320-
case *ColorPreview:
321-
_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
322-
}
323-
}
324-
return ast.WalkSkipChildren, nil
325-
}
326-
_, _ = w.WriteString("</code>")
327-
return ast.WalkContinue, nil
328-
}
329-
330-
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
331-
func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
332-
if entering {
333-
n := node.(*Attention)
334-
var octiconName string
335-
switch n.AttentionType {
336-
case "tip":
337-
octiconName = "light-bulb"
338-
case "important":
339-
octiconName = "report"
340-
case "warning":
341-
octiconName = "alert"
342-
case "caution":
343-
octiconName = "stop"
344-
default: // including "note"
345-
octiconName = "info"
346-
}
347-
_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
348-
}
349-
return ast.WalkContinue, nil
350-
}
351-
352138
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
353139
n := node.(*ast.Document)
354140

@@ -435,38 +221,3 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
435221

436222
return ast.WalkContinue, nil
437223
}
438-
439-
func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
440-
n := node.(*TaskCheckBoxListItem)
441-
if entering {
442-
if n.Attributes() != nil {
443-
_, _ = w.WriteString("<li")
444-
html.RenderAttributes(w, n, html.ListItemAttributeFilter)
445-
_ = w.WriteByte('>')
446-
} else {
447-
_, _ = w.WriteString("<li>")
448-
}
449-
fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
450-
if n.IsChecked {
451-
_, _ = w.WriteString(` checked=""`)
452-
}
453-
if r.XHTML {
454-
_, _ = w.WriteString(` />`)
455-
} else {
456-
_ = w.WriteByte('>')
457-
}
458-
fc := n.FirstChild()
459-
if fc != nil {
460-
if _, ok := fc.(*ast.TextBlock); !ok {
461-
_ = w.WriteByte('\n')
462-
}
463-
}
464-
} else {
465-
_, _ = w.WriteString("</li>\n")
466-
}
467-
return ast.WalkContinue, nil
468-
}
469-
470-
func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
471-
return ast.WalkContinue, nil
472-
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package markdown
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
10+
"code.gitea.io/gitea/modules/container"
11+
"code.gitea.io/gitea/modules/markup/common"
12+
13+
"github.com/yuin/goldmark/ast"
14+
"github.com/yuin/goldmark/util"
15+
)
16+
17+
type prefixedIDs struct {
18+
values container.Set[string]
19+
}
20+
21+
// Generate generates a new element id.
22+
func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
23+
dft := []byte("id")
24+
if kind == ast.KindHeading {
25+
dft = []byte("heading")
26+
}
27+
return p.GenerateWithDefault(value, dft)
28+
}
29+
30+
// GenerateWithDefault generates a new element id.
31+
func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
32+
result := common.CleanValue(value)
33+
if len(result) == 0 {
34+
result = dft
35+
}
36+
if !bytes.HasPrefix(result, []byte("user-content-")) {
37+
result = append([]byte("user-content-"), result...)
38+
}
39+
if p.values.Add(util.BytesToReadOnlyString(result)) {
40+
return result
41+
}
42+
for i := 1; ; i++ {
43+
newResult := fmt.Sprintf("%s-%d", result, i)
44+
if p.values.Add(newResult) {
45+
return []byte(newResult)
46+
}
47+
}
48+
}
49+
50+
// Put puts a given element id to the used ids table.
51+
func (p *prefixedIDs) Put(value []byte) {
52+
p.values.Add(util.BytesToReadOnlyString(value))
53+
}
54+
55+
func newPrefixedIDs() *prefixedIDs {
56+
return &prefixedIDs{
57+
values: make(container.Set[string]),
58+
}
59+
}

0 commit comments

Comments
 (0)