aboutsummaryrefslogtreecommitdiffstats
path: root/modules/markup/markdown
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-03-28 10:26:13 +0800
committerGitHub <noreply@github.com>2024-03-28 02:26:13 +0000
commit71706126b56616750a65290460fd211b9b8449da (patch)
tree1fa4b325af4c5821df86ff3d1461e7ac4beb7038 /modules/markup/markdown
parent7fda109aba6cd077343edef086b2f2ff60124f78 (diff)
downloadgitea-71706126b56616750a65290460fd211b9b8449da.tar.gz
gitea-71706126b56616750a65290460fd211b9b8449da.zip
Refactor markdown render (#30139)
Only split the file into small ones (and rename AttentionTypes to attentionTypes)
Diffstat (limited to 'modules/markup/markdown')
-rw-r--r--modules/markup/markdown/goldmark.go263
-rw-r--r--modules/markup/markdown/prefixed_id.go59
-rw-r--r--modules/markup/markdown/transform_blockquote.go27
-rw-r--r--modules/markup/markdown/transform_codespan.go57
-rw-r--r--modules/markup/markdown/transform_heading.go32
-rw-r--r--modules/markup/markdown/transform_image.go66
-rw-r--r--modules/markup/markdown/transform_link.go31
-rw-r--r--modules/markup/markdown/transform_list.go86
8 files changed, 364 insertions, 257 deletions
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index b61299c480..b8b3aeaab0 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -4,19 +4,14 @@
package markdown
import (
- "bytes"
"fmt"
"regexp"
"strings"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/common"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/svg"
- giteautil "code.gitea.io/gitea/modules/util"
- "github.com/microcosm-cc/bluemonday/css"
"github.com/yuin/goldmark/ast"
east "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
@@ -28,12 +23,12 @@ import (
// ASTTransformer is a default transformer of the goldmark tree.
type ASTTransformer struct {
- AttentionTypes container.Set[string]
+ attentionTypes container.Set[string]
}
func NewASTTransformer() *ASTTransformer {
return &ASTTransformer{
- AttentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
+ attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
}
}
@@ -66,123 +61,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
switch v := n.(type) {
case *ast.Heading:
- for _, attr := range v.Attributes() {
- if _, ok := attr.Value.([]byte); !ok {
- v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
- }
- }
- txt := n.Text(reader.Source())
- header := markup.Header{
- Text: util.BytesToReadOnlyString(txt),
- Level: v.Level,
- }
- if id, found := v.AttributeString("id"); found {
- header.ID = util.BytesToReadOnlyString(id.([]byte))
- }
- tocList = append(tocList, header)
- g.applyElementDir(v)
+ g.transformHeading(ctx, v, reader, &tocList)
case *ast.Paragraph:
g.applyElementDir(v)
case *ast.Image:
- // Images need two things:
- //
- // 1. Their src needs to munged to be a real value
- // 2. If they're not wrapped with a link they need a link wrapper
-
- // Check if the destination is a real link
- if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
- v.Destination = []byte(giteautil.URLJoin(
- ctx.Links.ResolveMediaLink(ctx.IsWiki),
- strings.TrimLeft(string(v.Destination), "/"),
- ))
- }
-
- parent := n.Parent()
- // Create a link around image only if parent is not already a link
- if _, ok := parent.(*ast.Link); !ok && parent != nil {
- next := n.NextSibling()
-
- // Create a link wrapper
- wrap := ast.NewLink()
- wrap.Destination = v.Destination
- wrap.Title = v.Title
- wrap.SetAttributeString("target", []byte("_blank"))
-
- // Duplicate the current image node
- image := ast.NewImage(ast.NewLink())
- image.Destination = v.Destination
- image.Title = v.Title
- for _, attr := range v.Attributes() {
- image.SetAttribute(attr.Name, attr.Value)
- }
- for child := v.FirstChild(); child != nil; {
- next := child.NextSibling()
- image.AppendChild(image, child)
- child = next
- }
-
- // Append our duplicate image to the wrapper link
- wrap.AppendChild(wrap, image)
-
- // Wire in the next sibling
- wrap.SetNextSibling(next)
-
- // Replace the current node with the wrapper link
- parent.ReplaceChild(parent, n, wrap)
-
- // But most importantly ensure the next sibling is still on the old image too
- v.SetNextSibling(next)
- }
+ g.transformImage(ctx, v, reader)
case *ast.Link:
- // Links need their href to munged to be a real value
- link := v.Destination
- isAnchorFragment := len(link) > 0 && link[0] == '#'
- if !isAnchorFragment && !markup.IsFullURLBytes(link) {
- base := ctx.Links.Base
- if ctx.IsWiki {
- base = ctx.Links.WikiLink()
- } else if ctx.Links.HasBranchInfo() {
- base = ctx.Links.SrcLink()
- }
- link = []byte(giteautil.URLJoin(base, string(link)))
- }
- if isAnchorFragment {
- link = []byte("#user-content-" + string(link)[1:])
- }
- v.Destination = link
+ g.transformLink(ctx, v, reader)
case *ast.List:
- if v.HasChildren() {
- children := make([]ast.Node, 0, v.ChildCount())
- child := v.FirstChild()
- for child != nil {
- children = append(children, child)
- child = child.NextSibling()
- }
- v.RemoveChildren(v)
-
- for _, child := range children {
- listItem := child.(*ast.ListItem)
- if !child.HasChildren() || !child.FirstChild().HasChildren() {
- v.AppendChild(v, child)
- continue
- }
- taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
- if !ok {
- v.AppendChild(v, child)
- continue
- }
- newChild := NewTaskCheckBoxListItem(listItem)
- newChild.IsChecked = taskCheckBox.IsChecked
- newChild.SetAttributeString("class", []byte("task-list-item"))
- segments := newChild.FirstChild().Lines()
- if segments.Len() > 0 {
- segment := segments.At(0)
- newChild.SourcePosition = rc.metaLength + segment.Start
- }
- v.AppendChild(v, newChild)
- }
- }
- g.applyElementDir(v)
+ g.transformList(ctx, v, reader, rc)
case *ast.Text:
if v.SoftLineBreak() && !v.HardLineBreak() {
if ctx.Metas["mode"] != "document" {
@@ -192,10 +79,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
}
}
case *ast.CodeSpan:
- colorContent := n.Text(reader.Source())
- if css.ColorHandler(strings.ToLower(string(colorContent))) {
- v.AppendChild(v, NewColorPreview(colorContent))
- }
+ g.transformCodeSpan(ctx, v, reader)
case *ast.Blockquote:
return g.transformBlockquote(v, reader)
}
@@ -219,50 +103,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
}
}
-type prefixedIDs struct {
- values container.Set[string]
-}
-
-// Generate generates a new element id.
-func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
- dft := []byte("id")
- if kind == ast.KindHeading {
- dft = []byte("heading")
- }
- return p.GenerateWithDefault(value, dft)
-}
-
-// GenerateWithDefault generates a new element id.
-func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
- result := common.CleanValue(value)
- if len(result) == 0 {
- result = dft
- }
- if !bytes.HasPrefix(result, []byte("user-content-")) {
- result = append([]byte("user-content-"), result...)
- }
- if p.values.Add(util.BytesToReadOnlyString(result)) {
- return result
- }
- for i := 1; ; i++ {
- newResult := fmt.Sprintf("%s-%d", result, i)
- if p.values.Add(newResult) {
- return []byte(newResult)
- }
- }
-}
-
-// Put puts a given element id to the used ids table.
-func (p *prefixedIDs) Put(value []byte) {
- p.values.Add(util.BytesToReadOnlyString(value))
-}
-
-func newPrefixedIDs() *prefixedIDs {
- return &prefixedIDs{
- values: make(container.Set[string]),
- }
-}
-
// NewHTMLRenderer creates a HTMLRenderer to render
// in the gitea form.
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
@@ -295,60 +135,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
}
-// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
-// See #21474 for reference
-func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
- if entering {
- if n.Attributes() != nil {
- _, _ = w.WriteString("<code")
- html.RenderAttributes(w, n, html.CodeAttributeFilter)
- _ = w.WriteByte('>')
- } else {
- _, _ = w.WriteString("<code>")
- }
- for c := n.FirstChild(); c != nil; c = c.NextSibling() {
- switch v := c.(type) {
- case *ast.Text:
- segment := v.Segment
- value := segment.Value(source)
- if bytes.HasSuffix(value, []byte("\n")) {
- r.Writer.RawWrite(w, value[:len(value)-1])
- r.Writer.RawWrite(w, []byte(" "))
- } else {
- r.Writer.RawWrite(w, value)
- }
- case *ColorPreview:
- _, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
- }
- }
- return ast.WalkSkipChildren, nil
- }
- _, _ = w.WriteString("</code>")
- return ast.WalkContinue, nil
-}
-
-// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
-func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- if entering {
- n := node.(*Attention)
- var octiconName string
- switch n.AttentionType {
- case "tip":
- octiconName = "light-bulb"
- case "important":
- octiconName = "report"
- case "warning":
- octiconName = "alert"
- case "caution":
- octiconName = "stop"
- default: // including "note"
- octiconName = "info"
- }
- _, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
- }
- return ast.WalkContinue, nil
-}
-
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Document)
@@ -435,38 +221,3 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
return ast.WalkContinue, nil
}
-
-func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- n := node.(*TaskCheckBoxListItem)
- if entering {
- if n.Attributes() != nil {
- _, _ = w.WriteString("<li")
- html.RenderAttributes(w, n, html.ListItemAttributeFilter)
- _ = w.WriteByte('>')
- } else {
- _, _ = w.WriteString("<li>")
- }
- fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
- if n.IsChecked {
- _, _ = w.WriteString(` checked=""`)
- }
- if r.XHTML {
- _, _ = w.WriteString(` />`)
- } else {
- _ = w.WriteByte('>')
- }
- fc := n.FirstChild()
- if fc != nil {
- if _, ok := fc.(*ast.TextBlock); !ok {
- _ = w.WriteByte('\n')
- }
- }
- } else {
- _, _ = w.WriteString("</li>\n")
- }
- return ast.WalkContinue, nil
-}
-
-func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- return ast.WalkContinue, nil
-}
diff --git a/modules/markup/markdown/prefixed_id.go b/modules/markup/markdown/prefixed_id.go
new file mode 100644
index 0000000000..9c60949202
--- /dev/null
+++ b/modules/markup/markdown/prefixed_id.go
@@ -0,0 +1,59 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "bytes"
+ "fmt"
+
+ "code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/markup/common"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/util"
+)
+
+type prefixedIDs struct {
+ values container.Set[string]
+}
+
+// Generate generates a new element id.
+func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
+ dft := []byte("id")
+ if kind == ast.KindHeading {
+ dft = []byte("heading")
+ }
+ return p.GenerateWithDefault(value, dft)
+}
+
+// GenerateWithDefault generates a new element id.
+func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
+ result := common.CleanValue(value)
+ if len(result) == 0 {
+ result = dft
+ }
+ if !bytes.HasPrefix(result, []byte("user-content-")) {
+ result = append([]byte("user-content-"), result...)
+ }
+ if p.values.Add(util.BytesToReadOnlyString(result)) {
+ return result
+ }
+ for i := 1; ; i++ {
+ newResult := fmt.Sprintf("%s-%d", result, i)
+ if p.values.Add(newResult) {
+ return []byte(newResult)
+ }
+ }
+}
+
+// Put puts a given element id to the used ids table.
+func (p *prefixedIDs) Put(value []byte) {
+ p.values.Add(util.BytesToReadOnlyString(value))
+}
+
+func newPrefixedIDs() *prefixedIDs {
+ return &prefixedIDs{
+ values: make(container.Set[string]),
+ }
+}
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index 65b735e83b..933f0e5c59 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -6,12 +6,37 @@ package markdown
import (
"strings"
+ "code.gitea.io/gitea/modules/svg"
+
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
+// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
+func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ n := node.(*Attention)
+ var octiconName string
+ switch n.AttentionType {
+ case "tip":
+ octiconName = "light-bulb"
+ case "important":
+ octiconName = "report"
+ case "warning":
+ octiconName = "alert"
+ case "caution":
+ octiconName = "stop"
+ default: // including "note"
+ octiconName = "info"
+ }
+ _, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
+ }
+ return ast.WalkContinue, nil
+}
+
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
// We only want attention blockquotes when the AST looks like:
// > Text("[") Text("!TYPE") Text("]")
@@ -43,7 +68,7 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
// grab attention type from markdown source
attentionType := strings.ToLower(val2[1:])
- if !g.AttentionTypes.Contains(attentionType) {
+ if !g.attentionTypes.Contains(attentionType) {
return ast.WalkContinue, nil
}
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
new file mode 100644
index 0000000000..bfff2897b0
--- /dev/null
+++ b/modules/markup/markdown/transform_codespan.go
@@ -0,0 +1,57 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/microcosm-cc/bluemonday/css"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
+// See #21474 for reference
+func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("<code")
+ html.RenderAttributes(w, n, html.CodeAttributeFilter)
+ _ = w.WriteByte('>')
+ } else {
+ _, _ = w.WriteString("<code>")
+ }
+ for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+ switch v := c.(type) {
+ case *ast.Text:
+ segment := v.Segment
+ value := segment.Value(source)
+ if bytes.HasSuffix(value, []byte("\n")) {
+ r.Writer.RawWrite(w, value[:len(value)-1])
+ r.Writer.RawWrite(w, []byte(" "))
+ } else {
+ r.Writer.RawWrite(w, value)
+ }
+ case *ColorPreview:
+ _, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
+ }
+ }
+ return ast.WalkSkipChildren, nil
+ }
+ _, _ = w.WriteString("</code>")
+ return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
+ colorContent := v.Text(reader.Source())
+ if css.ColorHandler(strings.ToLower(string(colorContent))) {
+ v.AppendChild(v, NewColorPreview(colorContent))
+ }
+}
diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go
new file mode 100644
index 0000000000..ce585a37de
--- /dev/null
+++ b/modules/markup/markdown/transform_heading.go
@@ -0,0 +1,32 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+func (g *ASTTransformer) transformHeading(ctx *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
+ for _, attr := range v.Attributes() {
+ if _, ok := attr.Value.([]byte); !ok {
+ v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
+ }
+ }
+ txt := v.Text(reader.Source())
+ header := markup.Header{
+ Text: util.BytesToReadOnlyString(txt),
+ Level: v.Level,
+ }
+ if id, found := v.AttributeString("id"); found {
+ header.ID = util.BytesToReadOnlyString(id.([]byte))
+ }
+ *tocList = append(*tocList, header)
+ g.applyElementDir(v)
+}
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go
new file mode 100644
index 0000000000..f290dc3721
--- /dev/null
+++ b/modules/markup/markdown/transform_image.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/markup"
+ giteautil "code.gitea.io/gitea/modules/util"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image, reader text.Reader) {
+ // Images need two things:
+ //
+ // 1. Their src needs to munged to be a real value
+ // 2. If they're not wrapped with a link they need a link wrapper
+
+ // Check if the destination is a real link
+ if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
+ v.Destination = []byte(giteautil.URLJoin(
+ ctx.Links.ResolveMediaLink(ctx.IsWiki),
+ strings.TrimLeft(string(v.Destination), "/"),
+ ))
+ }
+
+ parent := v.Parent()
+ // Create a link around image only if parent is not already a link
+ if _, ok := parent.(*ast.Link); !ok && parent != nil {
+ next := v.NextSibling()
+
+ // Create a link wrapper
+ wrap := ast.NewLink()
+ wrap.Destination = v.Destination
+ wrap.Title = v.Title
+ wrap.SetAttributeString("target", []byte("_blank"))
+
+ // Duplicate the current image node
+ image := ast.NewImage(ast.NewLink())
+ image.Destination = v.Destination
+ image.Title = v.Title
+ for _, attr := range v.Attributes() {
+ image.SetAttribute(attr.Name, attr.Value)
+ }
+ for child := v.FirstChild(); child != nil; {
+ next := child.NextSibling()
+ image.AppendChild(image, child)
+ child = next
+ }
+
+ // Append our duplicate image to the wrapper link
+ wrap.AppendChild(wrap, image)
+
+ // Wire in the next sibling
+ wrap.SetNextSibling(next)
+
+ // Replace the current node with the wrapper link
+ parent.ReplaceChild(parent, v, wrap)
+
+ // But most importantly ensure the next sibling is still on the old image too
+ v.SetNextSibling(next)
+ }
+}
diff --git a/modules/markup/markdown/transform_link.go b/modules/markup/markdown/transform_link.go
new file mode 100644
index 0000000000..8bf19ea4ce
--- /dev/null
+++ b/modules/markup/markdown/transform_link.go
@@ -0,0 +1,31 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "code.gitea.io/gitea/modules/markup"
+ giteautil "code.gitea.io/gitea/modules/util"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
+ // Links need their href to munged to be a real value
+ link := v.Destination
+ isAnchorFragment := len(link) > 0 && link[0] == '#'
+ if !isAnchorFragment && !markup.IsFullURLBytes(link) {
+ base := ctx.Links.Base
+ if ctx.IsWiki {
+ base = ctx.Links.WikiLink()
+ } else if ctx.Links.HasBranchInfo() {
+ base = ctx.Links.SrcLink()
+ }
+ link = []byte(giteautil.URLJoin(base, string(link)))
+ }
+ if isAnchorFragment {
+ link = []byte("#user-content-" + string(link)[1:])
+ }
+ v.Destination = link
+}
diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go
new file mode 100644
index 0000000000..6563e2dd64
--- /dev/null
+++ b/modules/markup/markdown/transform_list.go
@@ -0,0 +1,86 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/yuin/goldmark/ast"
+ east "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*TaskCheckBoxListItem)
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("<li")
+ html.RenderAttributes(w, n, html.ListItemAttributeFilter)
+ _ = w.WriteByte('>')
+ } else {
+ _, _ = w.WriteString("<li>")
+ }
+ fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
+ if n.IsChecked {
+ _, _ = w.WriteString(` checked=""`)
+ }
+ if r.XHTML {
+ _, _ = w.WriteString(` />`)
+ } else {
+ _ = w.WriteByte('>')
+ }
+ fc := n.FirstChild()
+ if fc != nil {
+ if _, ok := fc.(*ast.TextBlock); !ok {
+ _ = w.WriteByte('\n')
+ }
+ }
+ } else {
+ _, _ = w.WriteString("</li>\n")
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformList(ctx *markup.RenderContext, v *ast.List, reader text.Reader, rc *RenderConfig) {
+ if v.HasChildren() {
+ children := make([]ast.Node, 0, v.ChildCount())
+ child := v.FirstChild()
+ for child != nil {
+ children = append(children, child)
+ child = child.NextSibling()
+ }
+ v.RemoveChildren(v)
+
+ for _, child := range children {
+ listItem := child.(*ast.ListItem)
+ if !child.HasChildren() || !child.FirstChild().HasChildren() {
+ v.AppendChild(v, child)
+ continue
+ }
+ taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
+ if !ok {
+ v.AppendChild(v, child)
+ continue
+ }
+ newChild := NewTaskCheckBoxListItem(listItem)
+ newChild.IsChecked = taskCheckBox.IsChecked
+ newChild.SetAttributeString("class", []byte("task-list-item"))
+ segments := newChild.FirstChild().Lines()
+ if segments.Len() > 0 {
+ segment := segments.At(0)
+ newChild.SourcePosition = rc.metaLength + segment.Start
+ }
+ v.AppendChild(v, newChild)
+ }
+ }
+ g.applyElementDir(v)
+}