loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[PORT] gitea#30139: Refactor markdown render' (#3259) from algernon/forgejo:gitea/port/30139 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3259
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+358 -251
+12 -251
modules/markup/markdown/goldmark.go
··· 4 4 package markdown 5 5 6 6 import ( 7 - "bytes" 8 7 "fmt" 9 8 "regexp" 10 - "slices" 11 9 "strings" 12 10 13 - "code.gitea.io/gitea/modules/container" 14 11 "code.gitea.io/gitea/modules/markup" 15 - "code.gitea.io/gitea/modules/markup/common" 16 12 "code.gitea.io/gitea/modules/setting" 17 - giteautil "code.gitea.io/gitea/modules/util" 18 13 19 14 "github.com/yuin/goldmark/ast" 20 15 east "github.com/yuin/goldmark/extension/ast" ··· 30 25 // ASTTransformer is a default transformer of the goldmark tree. 31 26 type ASTTransformer struct{} 32 27 28 + func (g *ASTTransformer) applyElementDir(n ast.Node) { 29 + if markup.DefaultProcessorHelper.ElementDir != "" { 30 + n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir)) 31 + } 32 + } 33 + 33 34 // Transform transforms the given AST tree. 34 35 func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 35 36 firstChild := node.FirstChild() ··· 44 45 node.InsertBefore(node, firstChild, metaNode) 45 46 } 46 47 tocMode = rc.TOC 47 - } 48 - 49 - applyElementDir := func(n ast.Node) { 50 - if markup.DefaultProcessorHelper.ElementDir != "" { 51 - n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir)) 52 - } 53 48 } 54 49 55 50 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { ··· 59 54 60 55 switch v := n.(type) { 61 56 case *ast.Heading: 62 - for _, attr := range v.Attributes() { 63 - if _, ok := attr.Value.([]byte); !ok { 64 - v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) 65 - } 66 - } 67 - txt := n.Text(reader.Source()) 68 - header := markup.Header{ 69 - Text: util.BytesToReadOnlyString(txt), 70 - Level: v.Level, 71 - } 72 - if id, found := v.AttributeString("id"); found { 73 - header.ID = util.BytesToReadOnlyString(id.([]byte)) 74 - } 75 - tocList = append(tocList, header) 76 - applyElementDir(v) 57 + g.transformHeading(ctx, v, reader, &tocList) 77 58 case *ast.Paragraph: 78 - applyElementDir(v) 59 + g.applyElementDir(v) 79 60 case *ast.Image: 80 - // Images need two things: 81 - // 82 - // 1. Their src needs to munged to be a real value 83 - // 2. If they're not wrapped with a link they need a link wrapper 84 - 85 - // Check if the destination is a real link 86 - if len(v.Destination) > 0 && !markup.IsLink(v.Destination) { 87 - v.Destination = []byte(giteautil.URLJoin( 88 - ctx.Links.ResolveMediaLink(ctx.IsWiki), 89 - strings.TrimLeft(string(v.Destination), "/"), 90 - )) 91 - } 92 - 93 - parent := n.Parent() 94 - // Create a link around image only if parent is not already a link 95 - if _, ok := parent.(*ast.Link); !ok && parent != nil { 96 - next := n.NextSibling() 97 - 98 - // Create a link wrapper 99 - wrap := ast.NewLink() 100 - wrap.Destination = v.Destination 101 - wrap.Title = v.Title 102 - wrap.SetAttributeString("target", []byte("_blank")) 103 - 104 - // Duplicate the current image node 105 - image := ast.NewImage(ast.NewLink()) 106 - image.Destination = v.Destination 107 - image.Title = v.Title 108 - for _, attr := range v.Attributes() { 109 - image.SetAttribute(attr.Name, attr.Value) 110 - } 111 - for child := v.FirstChild(); child != nil; { 112 - next := child.NextSibling() 113 - image.AppendChild(image, child) 114 - child = next 115 - } 116 - 117 - // Append our duplicate image to the wrapper link 118 - wrap.AppendChild(wrap, image) 119 - 120 - // Wire in the next sibling 121 - wrap.SetNextSibling(next) 122 - 123 - // Replace the current node with the wrapper link 124 - parent.ReplaceChild(parent, n, wrap) 125 - 126 - // But most importantly ensure the next sibling is still on the old image too 127 - v.SetNextSibling(next) 128 - } 61 + g.transformImage(ctx, v, reader) 129 62 case *ast.Link: 130 - // Links need their href to munged to be a real value 131 - link := v.Destination 132 - 133 - // Do not process the link if it's not a link, starts with an hashtag 134 - // (indicating it's an anchor link), starts with `mailto:` or any of the 135 - // custom markdown URLs. 136 - processLink := len(link) > 0 && !markup.IsLink(link) && 137 - link[0] != '#' && !bytes.HasPrefix(link, byteMailto) && 138 - !slices.ContainsFunc(setting.Markdown.CustomURLSchemes, func(s string) bool { 139 - return bytes.HasPrefix(link, []byte(s+":")) 140 - }) 141 - 142 - if processLink { 143 - var base string 144 - if ctx.IsWiki { 145 - base = ctx.Links.WikiLink() 146 - } else if ctx.Links.HasBranchInfo() { 147 - base = ctx.Links.SrcLink() 148 - } else { 149 - base = ctx.Links.Base 150 - } 151 - 152 - link = []byte(giteautil.URLJoin(base, string(link))) 153 - } 154 - if len(link) > 0 && link[0] == '#' { 155 - link = []byte("#user-content-" + string(link)[1:]) 156 - } 157 - v.Destination = link 63 + g.transformLink(ctx, v, reader) 158 64 case *ast.List: 159 - if v.HasChildren() { 160 - children := make([]ast.Node, 0, v.ChildCount()) 161 - child := v.FirstChild() 162 - for child != nil { 163 - children = append(children, child) 164 - child = child.NextSibling() 165 - } 166 - v.RemoveChildren(v) 167 - 168 - for _, child := range children { 169 - listItem := child.(*ast.ListItem) 170 - if !child.HasChildren() || !child.FirstChild().HasChildren() { 171 - v.AppendChild(v, child) 172 - continue 173 - } 174 - taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox) 175 - if !ok { 176 - v.AppendChild(v, child) 177 - continue 178 - } 179 - newChild := NewTaskCheckBoxListItem(listItem) 180 - newChild.IsChecked = taskCheckBox.IsChecked 181 - newChild.SetAttributeString("class", []byte("task-list-item")) 182 - segments := newChild.FirstChild().Lines() 183 - if segments.Len() > 0 { 184 - segment := segments.At(0) 185 - newChild.SourcePosition = rc.metaLength + segment.Start 186 - } 187 - v.AppendChild(v, newChild) 188 - } 189 - } 190 - applyElementDir(v) 65 + g.transformList(ctx, v, reader, rc) 191 66 case *ast.Text: 192 67 if v.SoftLineBreak() && !v.HardLineBreak() { 193 68 if ctx.Metas["mode"] != "document" { ··· 197 72 } 198 73 } 199 74 case *ast.CodeSpan: 200 - colorContent := n.Text(reader.Source()) 201 - if matchColor(strings.ToLower(string(colorContent))) { 202 - v.AppendChild(v, NewColorPreview(colorContent)) 203 - } 75 + g.transformCodeSpan(ctx, v, reader) 204 76 } 205 77 return ast.WalkContinue, nil 206 78 }) ··· 222 94 } 223 95 } 224 96 225 - type prefixedIDs struct { 226 - values container.Set[string] 227 - } 228 - 229 - // Generate generates a new element id. 230 - func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte { 231 - dft := []byte("id") 232 - if kind == ast.KindHeading { 233 - dft = []byte("heading") 234 - } 235 - return p.GenerateWithDefault(value, dft) 236 - } 237 - 238 - // Generate generates a new element id. 239 - func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { 240 - result := common.CleanValue(value) 241 - if len(result) == 0 { 242 - result = dft 243 - } 244 - if !bytes.HasPrefix(result, []byte("user-content-")) { 245 - result = append([]byte("user-content-"), result...) 246 - } 247 - if p.values.Add(util.BytesToReadOnlyString(result)) { 248 - return result 249 - } 250 - for i := 1; ; i++ { 251 - newResult := fmt.Sprintf("%s-%d", result, i) 252 - if p.values.Add(newResult) { 253 - return []byte(newResult) 254 - } 255 - } 256 - } 257 - 258 - // Put puts a given element id to the used ids table. 259 - func (p *prefixedIDs) Put(value []byte) { 260 - p.values.Add(util.BytesToReadOnlyString(value)) 261 - } 262 - 263 - func newPrefixedIDs() *prefixedIDs { 264 - return &prefixedIDs{ 265 - values: make(container.Set[string]), 266 - } 267 - } 268 - 269 97 // NewHTMLRenderer creates a HTMLRenderer to render 270 98 // in the gitea form. 271 99 func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { ··· 295 123 reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) 296 124 } 297 125 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 126 func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 331 127 n := node.(*ast.Document) 332 128 ··· 415 211 416 212 return ast.WalkContinue, nil 417 213 } 418 - 419 - func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 420 - n := node.(*TaskCheckBoxListItem) 421 - if entering { 422 - if n.Attributes() != nil { 423 - _, _ = w.WriteString("<li") 424 - html.RenderAttributes(w, n, html.ListItemAttributeFilter) 425 - _ = w.WriteByte('>') 426 - } else { 427 - _, _ = w.WriteString("<li>") 428 - } 429 - fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition) 430 - if n.IsChecked { 431 - _, _ = w.WriteString(` checked=""`) 432 - } 433 - if r.XHTML { 434 - _, _ = w.WriteString(` />`) 435 - } else { 436 - _ = w.WriteByte('>') 437 - } 438 - fc := n.FirstChild() 439 - if fc != nil { 440 - if _, ok := fc.(*ast.TextBlock); !ok { 441 - _ = w.WriteByte('\n') 442 - } 443 - } 444 - } else { 445 - _, _ = w.WriteString("</li>\n") 446 - } 447 - return ast.WalkContinue, nil 448 - } 449 - 450 - func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 451 - return ast.WalkContinue, nil 452 - }
+59
modules/markup/markdown/prefixed_id.go
··· 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 + }
+56
modules/markup/markdown/transform_codespan.go
··· 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 + "strings" 10 + 11 + "code.gitea.io/gitea/modules/markup" 12 + 13 + "github.com/yuin/goldmark/ast" 14 + "github.com/yuin/goldmark/renderer/html" 15 + "github.com/yuin/goldmark/text" 16 + "github.com/yuin/goldmark/util" 17 + ) 18 + 19 + // renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements. 20 + // See #21474 for reference 21 + func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 22 + if entering { 23 + if n.Attributes() != nil { 24 + _, _ = w.WriteString("<code") 25 + html.RenderAttributes(w, n, html.CodeAttributeFilter) 26 + _ = w.WriteByte('>') 27 + } else { 28 + _, _ = w.WriteString("<code>") 29 + } 30 + for c := n.FirstChild(); c != nil; c = c.NextSibling() { 31 + switch v := c.(type) { 32 + case *ast.Text: 33 + segment := v.Segment 34 + value := segment.Value(source) 35 + if bytes.HasSuffix(value, []byte("\n")) { 36 + r.Writer.RawWrite(w, value[:len(value)-1]) 37 + r.Writer.RawWrite(w, []byte(" ")) 38 + } else { 39 + r.Writer.RawWrite(w, value) 40 + } 41 + case *ColorPreview: 42 + _, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color))) 43 + } 44 + } 45 + return ast.WalkSkipChildren, nil 46 + } 47 + _, _ = w.WriteString("</code>") 48 + return ast.WalkContinue, nil 49 + } 50 + 51 + func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) { 52 + colorContent := v.Text(reader.Source()) 53 + if matchColor(strings.ToLower(string(colorContent))) { 54 + v.AppendChild(v, NewColorPreview(colorContent)) 55 + } 56 + }
+32
modules/markup/markdown/transform_heading.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package markdown 5 + 6 + import ( 7 + "fmt" 8 + 9 + "code.gitea.io/gitea/modules/markup" 10 + 11 + "github.com/yuin/goldmark/ast" 12 + "github.com/yuin/goldmark/text" 13 + "github.com/yuin/goldmark/util" 14 + ) 15 + 16 + func (g *ASTTransformer) transformHeading(ctx *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) { 17 + for _, attr := range v.Attributes() { 18 + if _, ok := attr.Value.([]byte); !ok { 19 + v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) 20 + } 21 + } 22 + txt := v.Text(reader.Source()) 23 + header := markup.Header{ 24 + Text: util.BytesToReadOnlyString(txt), 25 + Level: v.Level, 26 + } 27 + if id, found := v.AttributeString("id"); found { 28 + header.ID = util.BytesToReadOnlyString(id.([]byte)) 29 + } 30 + *tocList = append(*tocList, header) 31 + g.applyElementDir(v) 32 + }
+66
modules/markup/markdown/transform_image.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package markdown 5 + 6 + import ( 7 + "strings" 8 + 9 + "code.gitea.io/gitea/modules/markup" 10 + giteautil "code.gitea.io/gitea/modules/util" 11 + 12 + "github.com/yuin/goldmark/ast" 13 + "github.com/yuin/goldmark/text" 14 + ) 15 + 16 + func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image, reader text.Reader) { 17 + // Images need two things: 18 + // 19 + // 1. Their src needs to munged to be a real value 20 + // 2. If they're not wrapped with a link they need a link wrapper 21 + 22 + // Check if the destination is a real link 23 + if len(v.Destination) > 0 && !markup.IsLink(v.Destination) { 24 + v.Destination = []byte(giteautil.URLJoin( 25 + ctx.Links.ResolveMediaLink(ctx.IsWiki), 26 + strings.TrimLeft(string(v.Destination), "/"), 27 + )) 28 + } 29 + 30 + parent := v.Parent() 31 + // Create a link around image only if parent is not already a link 32 + if _, ok := parent.(*ast.Link); !ok && parent != nil { 33 + next := v.NextSibling() 34 + 35 + // Create a link wrapper 36 + wrap := ast.NewLink() 37 + wrap.Destination = v.Destination 38 + wrap.Title = v.Title 39 + wrap.SetAttributeString("target", []byte("_blank")) 40 + 41 + // Duplicate the current image node 42 + image := ast.NewImage(ast.NewLink()) 43 + image.Destination = v.Destination 44 + image.Title = v.Title 45 + for _, attr := range v.Attributes() { 46 + image.SetAttribute(attr.Name, attr.Value) 47 + } 48 + for child := v.FirstChild(); child != nil; { 49 + next := child.NextSibling() 50 + image.AppendChild(image, child) 51 + child = next 52 + } 53 + 54 + // Append our duplicate image to the wrapper link 55 + wrap.AppendChild(wrap, image) 56 + 57 + // Wire in the next sibling 58 + wrap.SetNextSibling(next) 59 + 60 + // Replace the current node with the wrapper link 61 + parent.ReplaceChild(parent, v, wrap) 62 + 63 + // But most importantly ensure the next sibling is still on the old image too 64 + v.SetNextSibling(next) 65 + } 66 + }
+47
modules/markup/markdown/transform_link.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package markdown 5 + 6 + import ( 7 + "bytes" 8 + "slices" 9 + 10 + "code.gitea.io/gitea/modules/markup" 11 + "code.gitea.io/gitea/modules/setting" 12 + giteautil "code.gitea.io/gitea/modules/util" 13 + 14 + "github.com/yuin/goldmark/ast" 15 + "github.com/yuin/goldmark/text" 16 + ) 17 + 18 + func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) { 19 + // Links need their href to munged to be a real value 20 + link := v.Destination 21 + 22 + // Do not process the link if it's not a link, starts with an hashtag 23 + // (indicating it's an anchor link), starts with `mailto:` or any of the 24 + // custom markdown URLs. 25 + processLink := len(link) > 0 && !markup.IsLink(link) && 26 + link[0] != '#' && !bytes.HasPrefix(link, byteMailto) && 27 + !slices.ContainsFunc(setting.Markdown.CustomURLSchemes, func(s string) bool { 28 + return bytes.HasPrefix(link, []byte(s+":")) 29 + }) 30 + 31 + if processLink { 32 + var base string 33 + if ctx.IsWiki { 34 + base = ctx.Links.WikiLink() 35 + } else if ctx.Links.HasBranchInfo() { 36 + base = ctx.Links.SrcLink() 37 + } else { 38 + base = ctx.Links.Base 39 + } 40 + 41 + link = []byte(giteautil.URLJoin(base, string(link))) 42 + } 43 + if len(link) > 0 && link[0] == '#' { 44 + link = []byte("#user-content-" + string(link)[1:]) 45 + } 46 + v.Destination = link 47 + }
+86
modules/markup/markdown/transform_list.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package markdown 5 + 6 + import ( 7 + "fmt" 8 + 9 + "code.gitea.io/gitea/modules/markup" 10 + 11 + "github.com/yuin/goldmark/ast" 12 + east "github.com/yuin/goldmark/extension/ast" 13 + "github.com/yuin/goldmark/renderer/html" 14 + "github.com/yuin/goldmark/text" 15 + "github.com/yuin/goldmark/util" 16 + ) 17 + 18 + func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 19 + n := node.(*TaskCheckBoxListItem) 20 + if entering { 21 + if n.Attributes() != nil { 22 + _, _ = w.WriteString("<li") 23 + html.RenderAttributes(w, n, html.ListItemAttributeFilter) 24 + _ = w.WriteByte('>') 25 + } else { 26 + _, _ = w.WriteString("<li>") 27 + } 28 + fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition) 29 + if n.IsChecked { 30 + _, _ = w.WriteString(` checked=""`) 31 + } 32 + if r.XHTML { 33 + _, _ = w.WriteString(` />`) 34 + } else { 35 + _ = w.WriteByte('>') 36 + } 37 + fc := n.FirstChild() 38 + if fc != nil { 39 + if _, ok := fc.(*ast.TextBlock); !ok { 40 + _ = w.WriteByte('\n') 41 + } 42 + } 43 + } else { 44 + _, _ = w.WriteString("</li>\n") 45 + } 46 + return ast.WalkContinue, nil 47 + } 48 + 49 + func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 50 + return ast.WalkContinue, nil 51 + } 52 + 53 + func (g *ASTTransformer) transformList(ctx *markup.RenderContext, v *ast.List, reader text.Reader, rc *RenderConfig) { 54 + if v.HasChildren() { 55 + children := make([]ast.Node, 0, v.ChildCount()) 56 + child := v.FirstChild() 57 + for child != nil { 58 + children = append(children, child) 59 + child = child.NextSibling() 60 + } 61 + v.RemoveChildren(v) 62 + 63 + for _, child := range children { 64 + listItem := child.(*ast.ListItem) 65 + if !child.HasChildren() || !child.FirstChild().HasChildren() { 66 + v.AppendChild(v, child) 67 + continue 68 + } 69 + taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox) 70 + if !ok { 71 + v.AppendChild(v, child) 72 + continue 73 + } 74 + newChild := NewTaskCheckBoxListItem(listItem) 75 + newChild.IsChecked = taskCheckBox.IsChecked 76 + newChild.SetAttributeString("class", []byte("task-list-item")) 77 + segments := newChild.FirstChild().Lines() 78 + if segments.Len() > 0 { 79 + segment := segments.At(0) 80 + newChild.SourcePosition = rc.metaLength + segment.Start 81 + } 82 + v.AppendChild(v, newChild) 83 + } 84 + } 85 + g.applyElementDir(v) 86 + }