code.gitea.io/gitea@v1.19.3/modules/markup/markdown/goldmark.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package markdown 5 6 import ( 7 "bytes" 8 "fmt" 9 "regexp" 10 "strings" 11 12 "code.gitea.io/gitea/modules/container" 13 "code.gitea.io/gitea/modules/markup" 14 "code.gitea.io/gitea/modules/markup/common" 15 "code.gitea.io/gitea/modules/setting" 16 "code.gitea.io/gitea/modules/svg" 17 giteautil "code.gitea.io/gitea/modules/util" 18 19 "github.com/microcosm-cc/bluemonday/css" 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 firstChild := node.FirstChild() 37 createTOC := false 38 ctx := pc.Get(renderContextKey).(*markup.RenderContext) 39 rc := pc.Get(renderConfigKey).(*RenderConfig) 40 if rc.yamlNode != nil { 41 metaNode := rc.toMetaNode() 42 if metaNode != nil { 43 node.InsertBefore(node, firstChild, metaNode) 44 } 45 createTOC = rc.TOC 46 ctx.TableOfContents = make([]markup.Header, 0, 100) 47 } 48 49 attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote]) 50 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 51 if !entering { 52 return ast.WalkContinue, nil 53 } 54 55 switch v := n.(type) { 56 case *ast.Heading: 57 for _, attr := range v.Attributes() { 58 if _, ok := attr.Value.([]byte); !ok { 59 v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) 60 } 61 } 62 text := n.Text(reader.Source()) 63 header := markup.Header{ 64 Text: util.BytesToReadOnlyString(text), 65 Level: v.Level, 66 } 67 if id, found := v.AttributeString("id"); found { 68 header.ID = util.BytesToReadOnlyString(id.([]byte)) 69 } 70 ctx.TableOfContents = append(ctx.TableOfContents, header) 71 case *ast.Image: 72 // Images need two things: 73 // 74 // 1. Their src needs to munged to be a real value 75 // 2. If they're not wrapped with a link they need a link wrapper 76 77 // Check if the destination is a real link 78 link := v.Destination 79 if len(link) > 0 && !markup.IsLink(link) { 80 prefix := pc.Get(urlPrefixKey).(string) 81 if pc.Get(isWikiKey).(bool) { 82 prefix = giteautil.URLJoin(prefix, "wiki", "raw") 83 } 84 prefix = strings.Replace(prefix, "/src/", "/media/", 1) 85 86 lnk := strings.TrimLeft(string(link), "/") 87 88 lnk = giteautil.URLJoin(prefix, lnk) 89 link = []byte(lnk) 90 } 91 v.Destination = link 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 = link 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 = link 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 } 129 case *ast.Link: 130 // Links need their href to munged to be a real value 131 link := v.Destination 132 if len(link) > 0 && !markup.IsLink(link) && 133 link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { 134 // special case: this is not a link, a hash link or a mailto:, so it's a 135 // relative URL 136 lnk := string(link) 137 if pc.Get(isWikiKey).(bool) { 138 lnk = giteautil.URLJoin("wiki", lnk) 139 } 140 link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk)) 141 } 142 if len(link) > 0 && link[0] == '#' { 143 link = []byte("#user-content-" + string(link)[1:]) 144 } 145 v.Destination = link 146 case *ast.List: 147 if v.HasChildren() { 148 children := make([]ast.Node, 0, v.ChildCount()) 149 child := v.FirstChild() 150 for child != nil { 151 children = append(children, child) 152 child = child.NextSibling() 153 } 154 v.RemoveChildren(v) 155 156 for _, child := range children { 157 listItem := child.(*ast.ListItem) 158 if !child.HasChildren() || !child.FirstChild().HasChildren() { 159 v.AppendChild(v, child) 160 continue 161 } 162 taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox) 163 if !ok { 164 v.AppendChild(v, child) 165 continue 166 } 167 newChild := NewTaskCheckBoxListItem(listItem) 168 newChild.IsChecked = taskCheckBox.IsChecked 169 newChild.SetAttributeString("class", []byte("task-list-item")) 170 v.AppendChild(v, newChild) 171 } 172 } 173 case *ast.Text: 174 if v.SoftLineBreak() && !v.HardLineBreak() { 175 renderMetas := pc.Get(renderMetasKey).(map[string]string) 176 mode := renderMetas["mode"] 177 if mode != "document" { 178 v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) 179 } else { 180 v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) 181 } 182 } 183 case *ast.CodeSpan: 184 colorContent := n.Text(reader.Source()) 185 if css.ColorHandler(strings.ToLower(string(colorContent))) { 186 v.AppendChild(v, NewColorPreview(colorContent)) 187 } 188 case *ast.Emphasis: 189 // check if inside blockquote for attention, expected hierarchy is 190 // Emphasis < Paragraph < Blockquote 191 blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote) 192 if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) { 193 fullText := string(n.Text(reader.Source())) 194 if fullText == AttentionNote || fullText == AttentionWarning { 195 v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText))) 196 v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText)) 197 attentionMarkedBlockquotes.Add(blockquote) 198 } 199 } 200 } 201 return ast.WalkContinue, nil 202 }) 203 204 if createTOC && len(ctx.TableOfContents) > 0 { 205 lang := rc.Lang 206 if len(lang) == 0 { 207 lang = setting.Langs[0] 208 } 209 tocNode := createTOCNode(ctx.TableOfContents, lang) 210 if tocNode != nil { 211 node.InsertBefore(node, firstChild, tocNode) 212 } 213 } 214 215 if len(rc.Lang) > 0 { 216 node.SetAttributeString("lang", []byte(rc.Lang)) 217 } 218 } 219 220 type prefixedIDs struct { 221 values container.Set[string] 222 } 223 224 // Generate generates a new element id. 225 func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte { 226 dft := []byte("id") 227 if kind == ast.KindHeading { 228 dft = []byte("heading") 229 } 230 return p.GenerateWithDefault(value, dft) 231 } 232 233 // Generate generates a new element id. 234 func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { 235 result := common.CleanValue(value) 236 if len(result) == 0 { 237 result = dft 238 } 239 if !bytes.HasPrefix(result, []byte("user-content-")) { 240 result = append([]byte("user-content-"), result...) 241 } 242 if p.values.Add(util.BytesToReadOnlyString(result)) { 243 return result 244 } 245 for i := 1; ; i++ { 246 newResult := fmt.Sprintf("%s-%d", result, i) 247 if p.values.Add(newResult) { 248 return []byte(newResult) 249 } 250 } 251 } 252 253 // Put puts a given element id to the used ids table. 254 func (p *prefixedIDs) Put(value []byte) { 255 p.values.Add(util.BytesToReadOnlyString(value)) 256 } 257 258 func newPrefixedIDs() *prefixedIDs { 259 return &prefixedIDs{ 260 values: make(container.Set[string]), 261 } 262 } 263 264 // NewHTMLRenderer creates a HTMLRenderer to render 265 // in the gitea form. 266 func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 267 r := &HTMLRenderer{ 268 Config: html.NewConfig(), 269 } 270 for _, opt := range opts { 271 opt.SetHTMLOption(&r.Config) 272 } 273 return r 274 } 275 276 // HTMLRenderer is a renderer.NodeRenderer implementation that 277 // renders gitea specific features. 278 type HTMLRenderer struct { 279 html.Config 280 } 281 282 // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 283 func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 284 reg.Register(ast.KindDocument, r.renderDocument) 285 reg.Register(KindDetails, r.renderDetails) 286 reg.Register(KindSummary, r.renderSummary) 287 reg.Register(KindIcon, r.renderIcon) 288 reg.Register(ast.KindCodeSpan, r.renderCodeSpan) 289 reg.Register(KindAttention, r.renderAttention) 290 reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) 291 reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) 292 } 293 294 // renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements. 295 // See #21474 for reference 296 func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 297 if entering { 298 if n.Attributes() != nil { 299 _, _ = w.WriteString("<code") 300 html.RenderAttributes(w, n, html.CodeAttributeFilter) 301 _ = w.WriteByte('>') 302 } else { 303 _, _ = w.WriteString("<code>") 304 } 305 for c := n.FirstChild(); c != nil; c = c.NextSibling() { 306 switch v := c.(type) { 307 case *ast.Text: 308 segment := v.Segment 309 value := segment.Value(source) 310 if bytes.HasSuffix(value, []byte("\n")) { 311 r.Writer.RawWrite(w, value[:len(value)-1]) 312 r.Writer.RawWrite(w, []byte(" ")) 313 } else { 314 r.Writer.RawWrite(w, value) 315 } 316 case *ColorPreview: 317 _, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color))) 318 } 319 } 320 return ast.WalkSkipChildren, nil 321 } 322 _, _ = w.WriteString("</code>") 323 return ast.WalkContinue, nil 324 } 325 326 // renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg 327 func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 328 if entering { 329 _, _ = w.WriteString(`<span class="attention-icon attention-`) 330 n := node.(*Attention) 331 _, _ = w.WriteString(strings.ToLower(n.AttentionType)) 332 _, _ = w.WriteString(`">`) 333 334 var octiconType string 335 switch n.AttentionType { 336 case AttentionNote: 337 octiconType = "info" 338 case AttentionWarning: 339 octiconType = "alert" 340 } 341 _, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType))) 342 } else { 343 _, _ = w.WriteString("</span>\n") 344 } 345 return ast.WalkContinue, nil 346 } 347 348 func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 349 n := node.(*ast.Document) 350 351 if val, has := n.AttributeString("lang"); has { 352 var err error 353 if entering { 354 _, err = w.WriteString("<div") 355 if err == nil { 356 _, err = w.WriteString(fmt.Sprintf(` lang=%q`, val)) 357 } 358 if err == nil { 359 _, err = w.WriteRune('>') 360 } 361 } else { 362 _, err = w.WriteString("</div>") 363 } 364 365 if err != nil { 366 return ast.WalkStop, err 367 } 368 } 369 370 return ast.WalkContinue, nil 371 } 372 373 func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 374 var err error 375 if entering { 376 _, err = w.WriteString("<details>") 377 } else { 378 _, err = w.WriteString("</details>") 379 } 380 381 if err != nil { 382 return ast.WalkStop, err 383 } 384 385 return ast.WalkContinue, nil 386 } 387 388 func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 389 var err error 390 if entering { 391 _, err = w.WriteString("<summary>") 392 } else { 393 _, err = w.WriteString("</summary>") 394 } 395 396 if err != nil { 397 return ast.WalkStop, err 398 } 399 400 return ast.WalkContinue, nil 401 } 402 403 var validNameRE = regexp.MustCompile("^[a-z ]+$") 404 405 func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 406 if !entering { 407 return ast.WalkContinue, nil 408 } 409 410 n := node.(*Icon) 411 412 name := strings.TrimSpace(strings.ToLower(string(n.Name))) 413 414 if len(name) == 0 { 415 // skip this 416 return ast.WalkContinue, nil 417 } 418 419 if !validNameRE.MatchString(name) { 420 // skip this 421 return ast.WalkContinue, nil 422 } 423 424 var err error 425 _, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name)) 426 427 if err != nil { 428 return ast.WalkStop, err 429 } 430 431 return ast.WalkContinue, nil 432 } 433 434 func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 435 n := node.(*TaskCheckBoxListItem) 436 if entering { 437 if n.Attributes() != nil { 438 _, _ = w.WriteString("<li") 439 html.RenderAttributes(w, n, html.ListItemAttributeFilter) 440 _ = w.WriteByte('>') 441 } else { 442 _, _ = w.WriteString("<li>") 443 } 444 _, _ = w.WriteString(`<input type="checkbox" disabled=""`) 445 segments := node.FirstChild().Lines() 446 if segments.Len() > 0 { 447 segment := segments.At(0) 448 _, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start)) 449 } 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 }