github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/markup/markdown/goldmark.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 package markdown 7 8 import ( 9 "bytes" 10 "fmt" 11 "regexp" 12 "strings" 13 14 "github.com/gitbundle/modules/markup" 15 "github.com/gitbundle/modules/markup/common" 16 "github.com/gitbundle/modules/setting" 17 giteautil "github.com/gitbundle/modules/util" 18 19 meta "github.com/yuin/goldmark-meta" 20 "github.com/yuin/goldmark/ast" 21 east "github.com/yuin/goldmark/extension/ast" 22 "github.com/yuin/goldmark/parser" 23 "github.com/yuin/goldmark/renderer" 24 "github.com/yuin/goldmark/renderer/html" 25 "github.com/yuin/goldmark/text" 26 "github.com/yuin/goldmark/util" 27 ) 28 29 var byteMailto = []byte("mailto:") 30 31 // ASTTransformer is a default transformer of the goldmark tree. 32 type ASTTransformer struct{} 33 34 // Transform transforms the given AST tree. 35 func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 36 metaData := meta.GetItems(pc) 37 firstChild := node.FirstChild() 38 createTOC := false 39 ctx := pc.Get(renderContextKey).(*markup.RenderContext) 40 rc := &RenderConfig{ 41 Meta: "table", 42 Icon: "table", 43 Lang: "", 44 } 45 46 if metaData != nil { 47 rc.ToRenderConfig(metaData) 48 49 metaNode := rc.toMetaNode(metaData) 50 if metaNode != nil { 51 node.InsertBefore(node, firstChild, metaNode) 52 } 53 createTOC = rc.TOC 54 ctx.TableOfContents = make([]markup.Header, 0, 100) 55 } 56 57 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 58 if !entering { 59 return ast.WalkContinue, nil 60 } 61 62 switch v := n.(type) { 63 case *ast.Heading: 64 for _, attr := range v.Attributes() { 65 if _, ok := attr.Value.([]byte); !ok { 66 v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) 67 } 68 } 69 text := n.Text(reader.Source()) 70 header := markup.Header{ 71 Text: util.BytesToReadOnlyString(text), 72 Level: v.Level, 73 } 74 if id, found := v.AttributeString("id"); found { 75 header.ID = util.BytesToReadOnlyString(id.([]byte)) 76 } 77 ctx.TableOfContents = append(ctx.TableOfContents, header) 78 case *ast.Image: 79 // Images need two things: 80 // 81 // 1. Their src needs to munged to be a real value 82 // 2. If they're not wrapped with a link they need a link wrapper 83 84 // Check if the destination is a real link 85 link := v.Destination 86 if len(link) > 0 && !markup.IsLink(link) { 87 prefix := pc.Get(urlPrefixKey).(string) 88 if pc.Get(isWikiKey).(bool) { 89 prefix = giteautil.URLJoin(prefix, "wiki", "raw") 90 } 91 prefix = strings.Replace(prefix, "/src/", "/media/", 1) 92 93 lnk := strings.TrimLeft(string(link), "/") 94 95 lnk = giteautil.URLJoin(prefix, lnk) 96 link = []byte(lnk) 97 } 98 v.Destination = link 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 = link 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 = link 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 } 136 case *ast.Link: 137 // Links need their href to munged to be a real value 138 link := v.Destination 139 if len(link) > 0 && !markup.IsLink(link) && 140 link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { 141 // special case: this is not a link, a hash link or a mailto:, so it's a 142 // relative URL 143 lnk := string(link) 144 if pc.Get(isWikiKey).(bool) { 145 lnk = giteautil.URLJoin("wiki", lnk) 146 } 147 link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk)) 148 } 149 if len(link) > 0 && link[0] == '#' { 150 link = []byte("#user-content-" + string(link)[1:]) 151 } 152 v.Destination = link 153 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 v.AppendChild(v, newChild) 178 } 179 } 180 case *ast.Text: 181 if v.SoftLineBreak() && !v.HardLineBreak() { 182 renderMetas := pc.Get(renderMetasKey).(map[string]string) 183 mode := renderMetas["mode"] 184 if mode != "document" { 185 v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) 186 } else { 187 v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) 188 } 189 } 190 } 191 return ast.WalkContinue, nil 192 }) 193 194 if createTOC && len(ctx.TableOfContents) > 0 { 195 lang := rc.Lang 196 if len(lang) == 0 { 197 lang = setting.Langs[0] 198 } 199 tocNode := createTOCNode(ctx.TableOfContents, lang) 200 if tocNode != nil { 201 node.InsertBefore(node, firstChild, tocNode) 202 } 203 } 204 205 if len(rc.Lang) > 0 { 206 node.SetAttributeString("lang", []byte(rc.Lang)) 207 } 208 } 209 210 type prefixedIDs struct { 211 values map[string]bool 212 } 213 214 // Generate generates a new element id. 215 func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte { 216 dft := []byte("id") 217 if kind == ast.KindHeading { 218 dft = []byte("heading") 219 } 220 return p.GenerateWithDefault(value, dft) 221 } 222 223 // Generate generates a new element id. 224 func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { 225 result := common.CleanValue(value) 226 if len(result) == 0 { 227 result = dft 228 } 229 if !bytes.HasPrefix(result, []byte("user-content-")) { 230 result = append([]byte("user-content-"), result...) 231 } 232 if _, ok := p.values[util.BytesToReadOnlyString(result)]; !ok { 233 p.values[util.BytesToReadOnlyString(result)] = true 234 return result 235 } 236 for i := 1; ; i++ { 237 newResult := fmt.Sprintf("%s-%d", result, i) 238 if _, ok := p.values[newResult]; !ok { 239 p.values[newResult] = true 240 return []byte(newResult) 241 } 242 } 243 } 244 245 // Put puts a given element id to the used ids table. 246 func (p *prefixedIDs) Put(value []byte) { 247 p.values[util.BytesToReadOnlyString(value)] = true 248 } 249 250 func newPrefixedIDs() *prefixedIDs { 251 return &prefixedIDs{ 252 values: map[string]bool{}, 253 } 254 } 255 256 // NewHTMLRenderer creates a HTMLRenderer to render 257 // in the gitea form. 258 func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 259 r := &HTMLRenderer{ 260 Config: html.NewConfig(), 261 } 262 for _, opt := range opts { 263 opt.SetHTMLOption(&r.Config) 264 } 265 return r 266 } 267 268 // HTMLRenderer is a renderer.NodeRenderer implementation that 269 // renders gitea specific features. 270 type HTMLRenderer struct { 271 html.Config 272 } 273 274 // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 275 func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 276 reg.Register(ast.KindDocument, r.renderDocument) 277 reg.Register(KindDetails, r.renderDetails) 278 reg.Register(KindSummary, r.renderSummary) 279 reg.Register(KindIcon, r.renderIcon) 280 reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) 281 reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) 282 } 283 284 func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 285 n := node.(*ast.Document) 286 287 if val, has := n.AttributeString("lang"); has { 288 var err error 289 if entering { 290 _, err = w.WriteString("<div") 291 if err == nil { 292 _, err = w.WriteString(fmt.Sprintf(` lang=%q`, val)) 293 } 294 if err == nil { 295 _, err = w.WriteRune('>') 296 } 297 } else { 298 _, err = w.WriteString("</div>") 299 } 300 301 if err != nil { 302 return ast.WalkStop, err 303 } 304 } 305 306 return ast.WalkContinue, nil 307 } 308 309 func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 310 var err error 311 if entering { 312 _, err = w.WriteString("<details>") 313 } else { 314 _, err = w.WriteString("</details>") 315 } 316 317 if err != nil { 318 return ast.WalkStop, err 319 } 320 321 return ast.WalkContinue, nil 322 } 323 324 func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 325 var err error 326 if entering { 327 _, err = w.WriteString("<summary>") 328 } else { 329 _, err = w.WriteString("</summary>") 330 } 331 332 if err != nil { 333 return ast.WalkStop, err 334 } 335 336 return ast.WalkContinue, nil 337 } 338 339 var validNameRE = regexp.MustCompile("^[a-z ]+$") 340 341 func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 342 if !entering { 343 return ast.WalkContinue, nil 344 } 345 346 n := node.(*Icon) 347 348 name := strings.TrimSpace(strings.ToLower(string(n.Name))) 349 350 if len(name) == 0 { 351 // skip this 352 return ast.WalkContinue, nil 353 } 354 355 if !validNameRE.MatchString(name) { 356 // skip this 357 return ast.WalkContinue, nil 358 } 359 360 var err error 361 _, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name)) 362 363 if err != nil { 364 return ast.WalkStop, err 365 } 366 367 return ast.WalkContinue, nil 368 } 369 370 func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 371 n := node.(*TaskCheckBoxListItem) 372 if entering { 373 if n.Attributes() != nil { 374 _, _ = w.WriteString("<li") 375 html.RenderAttributes(w, n, html.ListItemAttributeFilter) 376 _ = w.WriteByte('>') 377 } else { 378 _, _ = w.WriteString("<li>") 379 } 380 _, _ = w.WriteString(`<input type="checkbox" disabled=""`) 381 segments := node.FirstChild().Lines() 382 if segments.Len() > 0 { 383 segment := segments.At(0) 384 _, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start)) 385 } 386 if n.IsChecked { 387 _, _ = w.WriteString(` checked=""`) 388 } 389 if r.XHTML { 390 _, _ = w.WriteString(` />`) 391 } else { 392 _ = w.WriteByte('>') 393 } 394 fc := n.FirstChild() 395 if fc != nil { 396 if _, ok := fc.(*ast.TextBlock); !ok { 397 _ = w.WriteByte('\n') 398 } 399 } 400 } else { 401 _, _ = w.WriteString("</li>\n") 402 } 403 return ast.WalkContinue, nil 404 } 405 406 func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 407 return ast.WalkContinue, nil 408 }