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