code.gitea.io/gitea@v1.19.3/modules/markup/common/footnote.go (about) 1 // Copyright 2019 Yusuke Inuzuka 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go 6 7 package common 8 9 import ( 10 "bytes" 11 "fmt" 12 "strconv" 13 "unicode" 14 15 "github.com/yuin/goldmark" 16 "github.com/yuin/goldmark/ast" 17 "github.com/yuin/goldmark/parser" 18 "github.com/yuin/goldmark/renderer" 19 "github.com/yuin/goldmark/renderer/html" 20 "github.com/yuin/goldmark/text" 21 "github.com/yuin/goldmark/util" 22 ) 23 24 // CleanValue will clean a value to make it safe to be an id 25 // This function is quite different from the original goldmark function 26 // and more closely matches the output from the shurcooL sanitizer 27 // In particular Unicode letters and numbers are a lot more than a-zA-Z0-9... 28 func CleanValue(value []byte) []byte { 29 value = bytes.TrimSpace(value) 30 rs := bytes.Runes(value) 31 result := make([]rune, 0, len(rs)) 32 needsDash := false 33 for _, r := range rs { 34 switch { 35 case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_': 36 if needsDash && len(result) > 0 { 37 result = append(result, '-') 38 } 39 needsDash = false 40 result = append(result, unicode.ToLower(r)) 41 default: 42 needsDash = true 43 } 44 } 45 return []byte(string(result)) 46 } 47 48 // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go 49 50 // A FootnoteLink struct represents a link to a footnote of Markdown 51 // (PHP Markdown Extra) text. 52 type FootnoteLink struct { 53 ast.BaseInline 54 Index int 55 Name []byte 56 } 57 58 // Dump implements Node.Dump. 59 func (n *FootnoteLink) Dump(source []byte, level int) { 60 m := map[string]string{} 61 m["Index"] = fmt.Sprintf("%v", n.Index) 62 m["Name"] = fmt.Sprintf("%v", n.Name) 63 ast.DumpHelper(n, source, level, m, nil) 64 } 65 66 // KindFootnoteLink is a NodeKind of the FootnoteLink node. 67 var KindFootnoteLink = ast.NewNodeKind("GiteaFootnoteLink") 68 69 // Kind implements Node.Kind. 70 func (n *FootnoteLink) Kind() ast.NodeKind { 71 return KindFootnoteLink 72 } 73 74 // NewFootnoteLink returns a new FootnoteLink node. 75 func NewFootnoteLink(index int, name []byte) *FootnoteLink { 76 return &FootnoteLink{ 77 Index: index, 78 Name: name, 79 } 80 } 81 82 // A FootnoteBackLink struct represents a link to a footnote of Markdown 83 // (PHP Markdown Extra) text. 84 type FootnoteBackLink struct { 85 ast.BaseInline 86 Index int 87 Name []byte 88 } 89 90 // Dump implements Node.Dump. 91 func (n *FootnoteBackLink) Dump(source []byte, level int) { 92 m := map[string]string{} 93 m["Index"] = fmt.Sprintf("%v", n.Index) 94 m["Name"] = fmt.Sprintf("%v", n.Name) 95 ast.DumpHelper(n, source, level, m, nil) 96 } 97 98 // KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node. 99 var KindFootnoteBackLink = ast.NewNodeKind("GiteaFootnoteBackLink") 100 101 // Kind implements Node.Kind. 102 func (n *FootnoteBackLink) Kind() ast.NodeKind { 103 return KindFootnoteBackLink 104 } 105 106 // NewFootnoteBackLink returns a new FootnoteBackLink node. 107 func NewFootnoteBackLink(index int, name []byte) *FootnoteBackLink { 108 return &FootnoteBackLink{ 109 Index: index, 110 Name: name, 111 } 112 } 113 114 // A Footnote struct represents a footnote of Markdown 115 // (PHP Markdown Extra) text. 116 type Footnote struct { 117 ast.BaseBlock 118 Ref []byte 119 Index int 120 Name []byte 121 } 122 123 // Dump implements Node.Dump. 124 func (n *Footnote) Dump(source []byte, level int) { 125 m := map[string]string{} 126 m["Index"] = strconv.Itoa(n.Index) 127 m["Ref"] = string(n.Ref) 128 m["Name"] = string(n.Name) 129 ast.DumpHelper(n, source, level, m, nil) 130 } 131 132 // KindFootnote is a NodeKind of the Footnote node. 133 var KindFootnote = ast.NewNodeKind("GiteaFootnote") 134 135 // Kind implements Node.Kind. 136 func (n *Footnote) Kind() ast.NodeKind { 137 return KindFootnote 138 } 139 140 // NewFootnote returns a new Footnote node. 141 func NewFootnote(ref []byte) *Footnote { 142 return &Footnote{ 143 Ref: ref, 144 Index: -1, 145 Name: ref, 146 } 147 } 148 149 // A FootnoteList struct represents footnotes of Markdown 150 // (PHP Markdown Extra) text. 151 type FootnoteList struct { 152 ast.BaseBlock 153 Count int 154 } 155 156 // Dump implements Node.Dump. 157 func (n *FootnoteList) Dump(source []byte, level int) { 158 m := map[string]string{} 159 m["Count"] = fmt.Sprintf("%v", n.Count) 160 ast.DumpHelper(n, source, level, m, nil) 161 } 162 163 // KindFootnoteList is a NodeKind of the FootnoteList node. 164 var KindFootnoteList = ast.NewNodeKind("GiteaFootnoteList") 165 166 // Kind implements Node.Kind. 167 func (n *FootnoteList) Kind() ast.NodeKind { 168 return KindFootnoteList 169 } 170 171 // NewFootnoteList returns a new FootnoteList node. 172 func NewFootnoteList() *FootnoteList { 173 return &FootnoteList{ 174 Count: 0, 175 } 176 } 177 178 var footnoteListKey = parser.NewContextKey() 179 180 type footnoteBlockParser struct{} 181 182 var defaultFootnoteBlockParser = &footnoteBlockParser{} 183 184 // NewFootnoteBlockParser returns a new parser.BlockParser that can parse 185 // footnotes of the Markdown(PHP Markdown Extra) text. 186 func NewFootnoteBlockParser() parser.BlockParser { 187 return defaultFootnoteBlockParser 188 } 189 190 func (b *footnoteBlockParser) Trigger() []byte { 191 return []byte{'['} 192 } 193 194 func (b *footnoteBlockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { 195 line, segment := reader.PeekLine() 196 pos := pc.BlockOffset() 197 if pos < 0 || line[pos] != '[' { 198 return nil, parser.NoChildren 199 } 200 pos++ 201 if pos > len(line)-1 || line[pos] != '^' { 202 return nil, parser.NoChildren 203 } 204 open := pos + 1 205 closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint 206 closes := pos + 1 + closure 207 next := closes + 1 208 if closure > -1 { 209 if next >= len(line) || line[next] != ':' { 210 return nil, parser.NoChildren 211 } 212 } else { 213 return nil, parser.NoChildren 214 } 215 padding := segment.Padding 216 label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding)) 217 if util.IsBlank(label) { 218 return nil, parser.NoChildren 219 } 220 item := NewFootnote(label) 221 222 pos = next + 1 - padding 223 if pos >= len(line) { 224 reader.Advance(pos) 225 return item, parser.NoChildren 226 } 227 reader.AdvanceAndSetPadding(pos, padding) 228 return item, parser.HasChildren 229 } 230 231 func (b *footnoteBlockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { 232 line, _ := reader.PeekLine() 233 if util.IsBlank(line) { 234 return parser.Continue | parser.HasChildren 235 } 236 childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4) 237 if childpos < 0 { 238 return parser.Close 239 } 240 reader.AdvanceAndSetPadding(childpos, padding) 241 return parser.Continue | parser.HasChildren 242 } 243 244 func (b *footnoteBlockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { 245 var list *FootnoteList 246 if tlist := pc.Get(footnoteListKey); tlist != nil { 247 list = tlist.(*FootnoteList) 248 } else { 249 list = NewFootnoteList() 250 pc.Set(footnoteListKey, list) 251 node.Parent().InsertBefore(node.Parent(), node, list) 252 } 253 node.Parent().RemoveChild(node.Parent(), node) 254 list.AppendChild(list, node) 255 } 256 257 func (b *footnoteBlockParser) CanInterruptParagraph() bool { 258 return true 259 } 260 261 func (b *footnoteBlockParser) CanAcceptIndentedLine() bool { 262 return false 263 } 264 265 type footnoteParser struct{} 266 267 var defaultFootnoteParser = &footnoteParser{} 268 269 // NewFootnoteParser returns a new parser.InlineParser that can parse 270 // footnote links of the Markdown(PHP Markdown Extra) text. 271 func NewFootnoteParser() parser.InlineParser { 272 return defaultFootnoteParser 273 } 274 275 func (s *footnoteParser) Trigger() []byte { 276 // footnote syntax probably conflict with the image syntax. 277 // So we need trigger this parser with '!'. 278 return []byte{'!', '['} 279 } 280 281 func (s *footnoteParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { 282 line, segment := block.PeekLine() 283 pos := 1 284 if len(line) > 0 && line[0] == '!' { 285 pos++ 286 } 287 if pos >= len(line) || line[pos] != '^' { 288 return nil 289 } 290 pos++ 291 if pos >= len(line) { 292 return nil 293 } 294 open := pos 295 closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint 296 if closure < 0 { 297 return nil 298 } 299 closes := pos + closure 300 value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes)) 301 block.Advance(closes + 1) 302 303 var list *FootnoteList 304 if tlist := pc.Get(footnoteListKey); tlist != nil { 305 list = tlist.(*FootnoteList) 306 } 307 if list == nil { 308 return nil 309 } 310 index := 0 311 name := []byte{} 312 for def := list.FirstChild(); def != nil; def = def.NextSibling() { 313 d := def.(*Footnote) 314 if bytes.Equal(d.Ref, value) { 315 if d.Index < 0 { 316 list.Count++ 317 d.Index = list.Count 318 val := CleanValue(d.Name) 319 if len(val) == 0 { 320 val = []byte(strconv.Itoa(d.Index)) 321 } 322 d.Name = pc.IDs().Generate(val, KindFootnote) 323 } 324 index = d.Index 325 name = d.Name 326 break 327 } 328 } 329 if index == 0 { 330 return nil 331 } 332 333 return NewFootnoteLink(index, name) 334 } 335 336 type footnoteASTTransformer struct{} 337 338 var defaultFootnoteASTTransformer = &footnoteASTTransformer{} 339 340 // NewFootnoteASTTransformer returns a new parser.ASTTransformer that 341 // insert a footnote list to the last of the document. 342 func NewFootnoteASTTransformer() parser.ASTTransformer { 343 return defaultFootnoteASTTransformer 344 } 345 346 func (a *footnoteASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 347 var list *FootnoteList 348 if tlist := pc.Get(footnoteListKey); tlist != nil { 349 list = tlist.(*FootnoteList) 350 } else { 351 return 352 } 353 pc.Set(footnoteListKey, nil) 354 for footnote := list.FirstChild(); footnote != nil; { 355 container := footnote 356 next := footnote.NextSibling() 357 if fc := container.LastChild(); fc != nil && ast.IsParagraph(fc) { 358 container = fc 359 } 360 footnoteNode := footnote.(*Footnote) 361 index := footnoteNode.Index 362 name := footnoteNode.Name 363 if index < 0 { 364 list.RemoveChild(list, footnote) 365 } else { 366 container.AppendChild(container, NewFootnoteBackLink(index, name)) 367 } 368 footnote = next 369 } 370 list.SortChildren(func(n1, n2 ast.Node) int { 371 if n1.(*Footnote).Index < n2.(*Footnote).Index { 372 return -1 373 } 374 return 1 375 }) 376 if list.Count <= 0 { 377 list.Parent().RemoveChild(list.Parent(), list) 378 return 379 } 380 381 node.AppendChild(node, list) 382 } 383 384 // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that 385 // renders FootnoteLink nodes. 386 type FootnoteHTMLRenderer struct { 387 html.Config 388 } 389 390 // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. 391 func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 392 r := &FootnoteHTMLRenderer{ 393 Config: html.NewConfig(), 394 } 395 for _, opt := range opts { 396 opt.SetHTMLOption(&r.Config) 397 } 398 return r 399 } 400 401 // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 402 func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 403 reg.Register(KindFootnoteLink, r.renderFootnoteLink) 404 reg.Register(KindFootnoteBackLink, r.renderFootnoteBackLink) 405 reg.Register(KindFootnote, r.renderFootnote) 406 reg.Register(KindFootnoteList, r.renderFootnoteList) 407 } 408 409 func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 410 if entering { 411 n := node.(*FootnoteLink) 412 is := strconv.Itoa(n.Index) 413 _, _ = w.WriteString(`<sup id="fnref:`) 414 _, _ = w.Write(n.Name) 415 _, _ = w.WriteString(`"><a href="#fn:`) 416 _, _ = w.Write(n.Name) 417 _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) 418 _, _ = w.WriteString(is) 419 _, _ = w.WriteString(`</a></sup>`) 420 } 421 return ast.WalkContinue, nil 422 } 423 424 func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 425 if entering { 426 n := node.(*FootnoteBackLink) 427 _, _ = w.WriteString(` <a href="#fnref:`) 428 _, _ = w.Write(n.Name) 429 _, _ = w.WriteString(`" class="footnote-backref" role="doc-backlink">`) 430 _, _ = w.WriteString("↩︎") 431 _, _ = w.WriteString(`</a>`) 432 } 433 return ast.WalkContinue, nil 434 } 435 436 func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 437 n := node.(*Footnote) 438 if entering { 439 _, _ = w.WriteString(`<li id="fn:`) 440 _, _ = w.Write(n.Name) 441 _, _ = w.WriteString(`" role="doc-endnote"`) 442 if node.Attributes() != nil { 443 html.RenderAttributes(w, node, html.ListItemAttributeFilter) 444 } 445 _, _ = w.WriteString(">\n") 446 } else { 447 _, _ = w.WriteString("</li>\n") 448 } 449 return ast.WalkContinue, nil 450 } 451 452 func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 453 tag := "div" 454 if entering { 455 _, _ = w.WriteString("<") 456 _, _ = w.WriteString(tag) 457 _, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`) 458 if node.Attributes() != nil { 459 html.RenderAttributes(w, node, html.GlobalAttributeFilter) 460 } 461 _ = w.WriteByte('>') 462 if r.Config.XHTML { 463 _, _ = w.WriteString("\n<hr />\n") 464 } else { 465 _, _ = w.WriteString("\n<hr>\n") 466 } 467 _, _ = w.WriteString("<ol>\n") 468 } else { 469 _, _ = w.WriteString("</ol>\n") 470 _, _ = w.WriteString("</") 471 _, _ = w.WriteString(tag) 472 _, _ = w.WriteString(">\n") 473 } 474 return ast.WalkContinue, nil 475 } 476 477 type footnoteExtension struct{} 478 479 // FootnoteExtension represents the Gitea Footnote 480 var FootnoteExtension = &footnoteExtension{} 481 482 // Extend extends the markdown converter with the Gitea Footnote parser 483 func (e *footnoteExtension) Extend(m goldmark.Markdown) { 484 m.Parser().AddOptions( 485 parser.WithBlockParsers( 486 util.Prioritized(NewFootnoteBlockParser(), 999), 487 ), 488 parser.WithInlineParsers( 489 util.Prioritized(NewFootnoteParser(), 101), 490 ), 491 parser.WithASTTransformers( 492 util.Prioritized(NewFootnoteASTTransformer(), 999), 493 ), 494 ) 495 m.Renderer().AddOptions(renderer.WithNodeRenderers( 496 util.Prioritized(NewFootnoteHTMLRenderer(), 500), 497 )) 498 }