···44package markdown
5566import (
77- "bytes"
87 "fmt"
98 "regexp"
1010- "slices"
119 "strings"
12101313- "code.gitea.io/gitea/modules/container"
1411 "code.gitea.io/gitea/modules/markup"
1515- "code.gitea.io/gitea/modules/markup/common"
1612 "code.gitea.io/gitea/modules/setting"
1717- giteautil "code.gitea.io/gitea/modules/util"
18131914 "github.com/yuin/goldmark/ast"
2015 east "github.com/yuin/goldmark/extension/ast"
···3025// ASTTransformer is a default transformer of the goldmark tree.
3126type ASTTransformer struct{}
32272828+func (g *ASTTransformer) applyElementDir(n ast.Node) {
2929+ if markup.DefaultProcessorHelper.ElementDir != "" {
3030+ n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
3131+ }
3232+}
3333+3334// Transform transforms the given AST tree.
3435func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
3536 firstChild := node.FirstChild()
···4445 node.InsertBefore(node, firstChild, metaNode)
4546 }
4647 tocMode = rc.TOC
4747- }
4848-4949- applyElementDir := func(n ast.Node) {
5050- if markup.DefaultProcessorHelper.ElementDir != "" {
5151- n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
5252- }
5348 }
54495550 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
···59546055 switch v := n.(type) {
6156 case *ast.Heading:
6262- for _, attr := range v.Attributes() {
6363- if _, ok := attr.Value.([]byte); !ok {
6464- v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
6565- }
6666- }
6767- txt := n.Text(reader.Source())
6868- header := markup.Header{
6969- Text: util.BytesToReadOnlyString(txt),
7070- Level: v.Level,
7171- }
7272- if id, found := v.AttributeString("id"); found {
7373- header.ID = util.BytesToReadOnlyString(id.([]byte))
7474- }
7575- tocList = append(tocList, header)
7676- applyElementDir(v)
5757+ g.transformHeading(ctx, v, reader, &tocList)
7758 case *ast.Paragraph:
7878- applyElementDir(v)
5959+ g.applyElementDir(v)
7960 case *ast.Image:
8080- // Images need two things:
8181- //
8282- // 1. Their src needs to munged to be a real value
8383- // 2. If they're not wrapped with a link they need a link wrapper
8484-8585- // Check if the destination is a real link
8686- if len(v.Destination) > 0 && !markup.IsLink(v.Destination) {
8787- v.Destination = []byte(giteautil.URLJoin(
8888- ctx.Links.ResolveMediaLink(ctx.IsWiki),
8989- strings.TrimLeft(string(v.Destination), "/"),
9090- ))
9191- }
9292-9393- parent := n.Parent()
9494- // Create a link around image only if parent is not already a link
9595- if _, ok := parent.(*ast.Link); !ok && parent != nil {
9696- next := n.NextSibling()
9797-9898- // Create a link wrapper
9999- wrap := ast.NewLink()
100100- wrap.Destination = v.Destination
101101- wrap.Title = v.Title
102102- wrap.SetAttributeString("target", []byte("_blank"))
103103-104104- // Duplicate the current image node
105105- image := ast.NewImage(ast.NewLink())
106106- image.Destination = v.Destination
107107- image.Title = v.Title
108108- for _, attr := range v.Attributes() {
109109- image.SetAttribute(attr.Name, attr.Value)
110110- }
111111- for child := v.FirstChild(); child != nil; {
112112- next := child.NextSibling()
113113- image.AppendChild(image, child)
114114- child = next
115115- }
116116-117117- // Append our duplicate image to the wrapper link
118118- wrap.AppendChild(wrap, image)
119119-120120- // Wire in the next sibling
121121- wrap.SetNextSibling(next)
122122-123123- // Replace the current node with the wrapper link
124124- parent.ReplaceChild(parent, n, wrap)
125125-126126- // But most importantly ensure the next sibling is still on the old image too
127127- v.SetNextSibling(next)
128128- }
6161+ g.transformImage(ctx, v, reader)
12962 case *ast.Link:
130130- // Links need their href to munged to be a real value
131131- link := v.Destination
132132-133133- // Do not process the link if it's not a link, starts with an hashtag
134134- // (indicating it's an anchor link), starts with `mailto:` or any of the
135135- // custom markdown URLs.
136136- processLink := len(link) > 0 && !markup.IsLink(link) &&
137137- link[0] != '#' && !bytes.HasPrefix(link, byteMailto) &&
138138- !slices.ContainsFunc(setting.Markdown.CustomURLSchemes, func(s string) bool {
139139- return bytes.HasPrefix(link, []byte(s+":"))
140140- })
141141-142142- if processLink {
143143- var base string
144144- if ctx.IsWiki {
145145- base = ctx.Links.WikiLink()
146146- } else if ctx.Links.HasBranchInfo() {
147147- base = ctx.Links.SrcLink()
148148- } else {
149149- base = ctx.Links.Base
150150- }
151151-152152- link = []byte(giteautil.URLJoin(base, string(link)))
153153- }
154154- if len(link) > 0 && link[0] == '#' {
155155- link = []byte("#user-content-" + string(link)[1:])
156156- }
157157- v.Destination = link
6363+ g.transformLink(ctx, v, reader)
15864 case *ast.List:
159159- if v.HasChildren() {
160160- children := make([]ast.Node, 0, v.ChildCount())
161161- child := v.FirstChild()
162162- for child != nil {
163163- children = append(children, child)
164164- child = child.NextSibling()
165165- }
166166- v.RemoveChildren(v)
167167-168168- for _, child := range children {
169169- listItem := child.(*ast.ListItem)
170170- if !child.HasChildren() || !child.FirstChild().HasChildren() {
171171- v.AppendChild(v, child)
172172- continue
173173- }
174174- taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
175175- if !ok {
176176- v.AppendChild(v, child)
177177- continue
178178- }
179179- newChild := NewTaskCheckBoxListItem(listItem)
180180- newChild.IsChecked = taskCheckBox.IsChecked
181181- newChild.SetAttributeString("class", []byte("task-list-item"))
182182- segments := newChild.FirstChild().Lines()
183183- if segments.Len() > 0 {
184184- segment := segments.At(0)
185185- newChild.SourcePosition = rc.metaLength + segment.Start
186186- }
187187- v.AppendChild(v, newChild)
188188- }
189189- }
190190- applyElementDir(v)
6565+ g.transformList(ctx, v, reader, rc)
19166 case *ast.Text:
19267 if v.SoftLineBreak() && !v.HardLineBreak() {
19368 if ctx.Metas["mode"] != "document" {
···19772 }
19873 }
19974 case *ast.CodeSpan:
200200- colorContent := n.Text(reader.Source())
201201- if matchColor(strings.ToLower(string(colorContent))) {
202202- v.AppendChild(v, NewColorPreview(colorContent))
203203- }
7575+ g.transformCodeSpan(ctx, v, reader)
20476 }
20577 return ast.WalkContinue, nil
20678 })
···22294 }
22395}
22496225225-type prefixedIDs struct {
226226- values container.Set[string]
227227-}
228228-229229-// Generate generates a new element id.
230230-func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
231231- dft := []byte("id")
232232- if kind == ast.KindHeading {
233233- dft = []byte("heading")
234234- }
235235- return p.GenerateWithDefault(value, dft)
236236-}
237237-238238-// Generate generates a new element id.
239239-func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
240240- result := common.CleanValue(value)
241241- if len(result) == 0 {
242242- result = dft
243243- }
244244- if !bytes.HasPrefix(result, []byte("user-content-")) {
245245- result = append([]byte("user-content-"), result...)
246246- }
247247- if p.values.Add(util.BytesToReadOnlyString(result)) {
248248- return result
249249- }
250250- for i := 1; ; i++ {
251251- newResult := fmt.Sprintf("%s-%d", result, i)
252252- if p.values.Add(newResult) {
253253- return []byte(newResult)
254254- }
255255- }
256256-}
257257-258258-// Put puts a given element id to the used ids table.
259259-func (p *prefixedIDs) Put(value []byte) {
260260- p.values.Add(util.BytesToReadOnlyString(value))
261261-}
262262-263263-func newPrefixedIDs() *prefixedIDs {
264264- return &prefixedIDs{
265265- values: make(container.Set[string]),
266266- }
267267-}
268268-26997// NewHTMLRenderer creates a HTMLRenderer to render
27098// in the gitea form.
27199func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
···295123 reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
296124}
297125298298-// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
299299-// See #21474 for reference
300300-func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
301301- if entering {
302302- if n.Attributes() != nil {
303303- _, _ = w.WriteString("<code")
304304- html.RenderAttributes(w, n, html.CodeAttributeFilter)
305305- _ = w.WriteByte('>')
306306- } else {
307307- _, _ = w.WriteString("<code>")
308308- }
309309- for c := n.FirstChild(); c != nil; c = c.NextSibling() {
310310- switch v := c.(type) {
311311- case *ast.Text:
312312- segment := v.Segment
313313- value := segment.Value(source)
314314- if bytes.HasSuffix(value, []byte("\n")) {
315315- r.Writer.RawWrite(w, value[:len(value)-1])
316316- r.Writer.RawWrite(w, []byte(" "))
317317- } else {
318318- r.Writer.RawWrite(w, value)
319319- }
320320- case *ColorPreview:
321321- _, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
322322- }
323323- }
324324- return ast.WalkSkipChildren, nil
325325- }
326326- _, _ = w.WriteString("</code>")
327327- return ast.WalkContinue, nil
328328-}
329329-330126func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
331127 n := node.(*ast.Document)
332128···415211416212 return ast.WalkContinue, nil
417213}
418418-419419-func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
420420- n := node.(*TaskCheckBoxListItem)
421421- if entering {
422422- if n.Attributes() != nil {
423423- _, _ = w.WriteString("<li")
424424- html.RenderAttributes(w, n, html.ListItemAttributeFilter)
425425- _ = w.WriteByte('>')
426426- } else {
427427- _, _ = w.WriteString("<li>")
428428- }
429429- fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
430430- if n.IsChecked {
431431- _, _ = w.WriteString(` checked=""`)
432432- }
433433- if r.XHTML {
434434- _, _ = w.WriteString(` />`)
435435- } else {
436436- _ = w.WriteByte('>')
437437- }
438438- fc := n.FirstChild()
439439- if fc != nil {
440440- if _, ok := fc.(*ast.TextBlock); !ok {
441441- _ = w.WriteByte('\n')
442442- }
443443- }
444444- } else {
445445- _, _ = w.WriteString("</li>\n")
446446- }
447447- return ast.WalkContinue, nil
448448-}
449449-450450-func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
451451- return ast.WalkContinue, nil
452452-}
+59
modules/markup/markdown/prefixed_id.go
···11+// Copyright 2024 The Gitea Authors. All rights reserved.
22+// SPDX-License-Identifier: MIT
33+44+package markdown
55+66+import (
77+ "bytes"
88+ "fmt"
99+1010+ "code.gitea.io/gitea/modules/container"
1111+ "code.gitea.io/gitea/modules/markup/common"
1212+1313+ "github.com/yuin/goldmark/ast"
1414+ "github.com/yuin/goldmark/util"
1515+)
1616+1717+type prefixedIDs struct {
1818+ values container.Set[string]
1919+}
2020+2121+// Generate generates a new element id.
2222+func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
2323+ dft := []byte("id")
2424+ if kind == ast.KindHeading {
2525+ dft = []byte("heading")
2626+ }
2727+ return p.GenerateWithDefault(value, dft)
2828+}
2929+3030+// GenerateWithDefault generates a new element id.
3131+func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
3232+ result := common.CleanValue(value)
3333+ if len(result) == 0 {
3434+ result = dft
3535+ }
3636+ if !bytes.HasPrefix(result, []byte("user-content-")) {
3737+ result = append([]byte("user-content-"), result...)
3838+ }
3939+ if p.values.Add(util.BytesToReadOnlyString(result)) {
4040+ return result
4141+ }
4242+ for i := 1; ; i++ {
4343+ newResult := fmt.Sprintf("%s-%d", result, i)
4444+ if p.values.Add(newResult) {
4545+ return []byte(newResult)
4646+ }
4747+ }
4848+}
4949+5050+// Put puts a given element id to the used ids table.
5151+func (p *prefixedIDs) Put(value []byte) {
5252+ p.values.Add(util.BytesToReadOnlyString(value))
5353+}
5454+5555+func newPrefixedIDs() *prefixedIDs {
5656+ return &prefixedIDs{
5757+ values: make(container.Set[string]),
5858+ }
5959+}
+56
modules/markup/markdown/transform_codespan.go
···11+// Copyright 2024 The Gitea Authors. All rights reserved.
22+// SPDX-License-Identifier: MIT
33+44+package markdown
55+66+import (
77+ "bytes"
88+ "fmt"
99+ "strings"
1010+1111+ "code.gitea.io/gitea/modules/markup"
1212+1313+ "github.com/yuin/goldmark/ast"
1414+ "github.com/yuin/goldmark/renderer/html"
1515+ "github.com/yuin/goldmark/text"
1616+ "github.com/yuin/goldmark/util"
1717+)
1818+1919+// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
2020+// See #21474 for reference
2121+func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
2222+ if entering {
2323+ if n.Attributes() != nil {
2424+ _, _ = w.WriteString("<code")
2525+ html.RenderAttributes(w, n, html.CodeAttributeFilter)
2626+ _ = w.WriteByte('>')
2727+ } else {
2828+ _, _ = w.WriteString("<code>")
2929+ }
3030+ for c := n.FirstChild(); c != nil; c = c.NextSibling() {
3131+ switch v := c.(type) {
3232+ case *ast.Text:
3333+ segment := v.Segment
3434+ value := segment.Value(source)
3535+ if bytes.HasSuffix(value, []byte("\n")) {
3636+ r.Writer.RawWrite(w, value[:len(value)-1])
3737+ r.Writer.RawWrite(w, []byte(" "))
3838+ } else {
3939+ r.Writer.RawWrite(w, value)
4040+ }
4141+ case *ColorPreview:
4242+ _, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
4343+ }
4444+ }
4545+ return ast.WalkSkipChildren, nil
4646+ }
4747+ _, _ = w.WriteString("</code>")
4848+ return ast.WalkContinue, nil
4949+}
5050+5151+func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
5252+ colorContent := v.Text(reader.Source())
5353+ if matchColor(strings.ToLower(string(colorContent))) {
5454+ v.AppendChild(v, NewColorPreview(colorContent))
5555+ }
5656+}
+32
modules/markup/markdown/transform_heading.go
···11+// Copyright 2024 The Gitea Authors. All rights reserved.
22+// SPDX-License-Identifier: MIT
33+44+package markdown
55+66+import (
77+ "fmt"
88+99+ "code.gitea.io/gitea/modules/markup"
1010+1111+ "github.com/yuin/goldmark/ast"
1212+ "github.com/yuin/goldmark/text"
1313+ "github.com/yuin/goldmark/util"
1414+)
1515+1616+func (g *ASTTransformer) transformHeading(ctx *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
1717+ for _, attr := range v.Attributes() {
1818+ if _, ok := attr.Value.([]byte); !ok {
1919+ v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
2020+ }
2121+ }
2222+ txt := v.Text(reader.Source())
2323+ header := markup.Header{
2424+ Text: util.BytesToReadOnlyString(txt),
2525+ Level: v.Level,
2626+ }
2727+ if id, found := v.AttributeString("id"); found {
2828+ header.ID = util.BytesToReadOnlyString(id.([]byte))
2929+ }
3030+ *tocList = append(*tocList, header)
3131+ g.applyElementDir(v)
3232+}
+66
modules/markup/markdown/transform_image.go
···11+// Copyright 2024 The Gitea Authors. All rights reserved.
22+// SPDX-License-Identifier: MIT
33+44+package markdown
55+66+import (
77+ "strings"
88+99+ "code.gitea.io/gitea/modules/markup"
1010+ giteautil "code.gitea.io/gitea/modules/util"
1111+1212+ "github.com/yuin/goldmark/ast"
1313+ "github.com/yuin/goldmark/text"
1414+)
1515+1616+func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image, reader text.Reader) {
1717+ // Images need two things:
1818+ //
1919+ // 1. Their src needs to munged to be a real value
2020+ // 2. If they're not wrapped with a link they need a link wrapper
2121+2222+ // Check if the destination is a real link
2323+ if len(v.Destination) > 0 && !markup.IsLink(v.Destination) {
2424+ v.Destination = []byte(giteautil.URLJoin(
2525+ ctx.Links.ResolveMediaLink(ctx.IsWiki),
2626+ strings.TrimLeft(string(v.Destination), "/"),
2727+ ))
2828+ }
2929+3030+ parent := v.Parent()
3131+ // Create a link around image only if parent is not already a link
3232+ if _, ok := parent.(*ast.Link); !ok && parent != nil {
3333+ next := v.NextSibling()
3434+3535+ // Create a link wrapper
3636+ wrap := ast.NewLink()
3737+ wrap.Destination = v.Destination
3838+ wrap.Title = v.Title
3939+ wrap.SetAttributeString("target", []byte("_blank"))
4040+4141+ // Duplicate the current image node
4242+ image := ast.NewImage(ast.NewLink())
4343+ image.Destination = v.Destination
4444+ image.Title = v.Title
4545+ for _, attr := range v.Attributes() {
4646+ image.SetAttribute(attr.Name, attr.Value)
4747+ }
4848+ for child := v.FirstChild(); child != nil; {
4949+ next := child.NextSibling()
5050+ image.AppendChild(image, child)
5151+ child = next
5252+ }
5353+5454+ // Append our duplicate image to the wrapper link
5555+ wrap.AppendChild(wrap, image)
5656+5757+ // Wire in the next sibling
5858+ wrap.SetNextSibling(next)
5959+6060+ // Replace the current node with the wrapper link
6161+ parent.ReplaceChild(parent, v, wrap)
6262+6363+ // But most importantly ensure the next sibling is still on the old image too
6464+ v.SetNextSibling(next)
6565+ }
6666+}
+47
modules/markup/markdown/transform_link.go
···11+// Copyright 2024 The Gitea Authors. All rights reserved.
22+// SPDX-License-Identifier: MIT
33+44+package markdown
55+66+import (
77+ "bytes"
88+ "slices"
99+1010+ "code.gitea.io/gitea/modules/markup"
1111+ "code.gitea.io/gitea/modules/setting"
1212+ giteautil "code.gitea.io/gitea/modules/util"
1313+1414+ "github.com/yuin/goldmark/ast"
1515+ "github.com/yuin/goldmark/text"
1616+)
1717+1818+func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
1919+ // Links need their href to munged to be a real value
2020+ link := v.Destination
2121+2222+ // Do not process the link if it's not a link, starts with an hashtag
2323+ // (indicating it's an anchor link), starts with `mailto:` or any of the
2424+ // custom markdown URLs.
2525+ processLink := len(link) > 0 && !markup.IsLink(link) &&
2626+ link[0] != '#' && !bytes.HasPrefix(link, byteMailto) &&
2727+ !slices.ContainsFunc(setting.Markdown.CustomURLSchemes, func(s string) bool {
2828+ return bytes.HasPrefix(link, []byte(s+":"))
2929+ })
3030+3131+ if processLink {
3232+ var base string
3333+ if ctx.IsWiki {
3434+ base = ctx.Links.WikiLink()
3535+ } else if ctx.Links.HasBranchInfo() {
3636+ base = ctx.Links.SrcLink()
3737+ } else {
3838+ base = ctx.Links.Base
3939+ }
4040+4141+ link = []byte(giteautil.URLJoin(base, string(link)))
4242+ }
4343+ if len(link) > 0 && link[0] == '#' {
4444+ link = []byte("#user-content-" + string(link)[1:])
4545+ }
4646+ v.Destination = link
4747+}