1package extension 2 3import ( 4 "bytes" 5 "strconv" 6 7 "github.com/yuin/goldmark" 8 gast "github.com/yuin/goldmark/ast" 9 "github.com/yuin/goldmark/extension/ast" 10 "github.com/yuin/goldmark/parser" 11 "github.com/yuin/goldmark/renderer" 12 "github.com/yuin/goldmark/renderer/html" 13 "github.com/yuin/goldmark/text" 14 "github.com/yuin/goldmark/util" 15) 16 17var footnoteListKey = parser.NewContextKey() 18var footnoteLinkListKey = parser.NewContextKey() 19 20type footnoteBlockParser struct { 21} 22 23var defaultFootnoteBlockParser = &footnoteBlockParser{} 24 25// NewFootnoteBlockParser returns a new parser.BlockParser that can parse 26// footnotes of the Markdown(PHP Markdown Extra) text. 27func NewFootnoteBlockParser() parser.BlockParser { 28 return defaultFootnoteBlockParser 29} 30 31func (b *footnoteBlockParser) Trigger() []byte { 32 return []byte{'['} 33} 34 35func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { 36 line, segment := reader.PeekLine() 37 pos := pc.BlockOffset() 38 if pos < 0 || line[pos] != '[' { 39 return nil, parser.NoChildren 40 } 41 pos++ 42 if pos > len(line)-1 || line[pos] != '^' { 43 return nil, parser.NoChildren 44 } 45 open := pos + 1 46 closes := 0 47 closure := util.FindClosure(line[pos+1:], '[', ']', false, false) 48 closes = pos + 1 + closure 49 next := closes + 1 50 if closure > -1 { 51 if next >= len(line) || line[next] != ':' { 52 return nil, parser.NoChildren 53 } 54 } else { 55 return nil, parser.NoChildren 56 } 57 padding := segment.Padding 58 label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding)) 59 if util.IsBlank(label) { 60 return nil, parser.NoChildren 61 } 62 item := ast.NewFootnote(label) 63 64 pos = next + 1 - padding 65 if pos >= len(line) { 66 reader.Advance(pos) 67 return item, parser.NoChildren 68 } 69 reader.AdvanceAndSetPadding(pos, padding) 70 return item, parser.HasChildren 71} 72 73func (b *footnoteBlockParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { 74 line, _ := reader.PeekLine() 75 if util.IsBlank(line) { 76 return parser.Continue | parser.HasChildren 77 } 78 childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4) 79 if childpos < 0 { 80 return parser.Close 81 } 82 reader.AdvanceAndSetPadding(childpos, padding) 83 return parser.Continue | parser.HasChildren 84} 85 86func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { 87 var list *ast.FootnoteList 88 if tlist := pc.Get(footnoteListKey); tlist != nil { 89 list = tlist.(*ast.FootnoteList) 90 } else { 91 list = ast.NewFootnoteList() 92 pc.Set(footnoteListKey, list) 93 node.Parent().InsertBefore(node.Parent(), node, list) 94 } 95 node.Parent().RemoveChild(node.Parent(), node) 96 list.AppendChild(list, node) 97} 98 99func (b *footnoteBlockParser) CanInterruptParagraph() bool { 100 return true 101} 102 103func (b *footnoteBlockParser) CanAcceptIndentedLine() bool { 104 return false 105} 106 107type footnoteParser struct { 108} 109 110var defaultFootnoteParser = &footnoteParser{} 111 112// NewFootnoteParser returns a new parser.InlineParser that can parse 113// footnote links of the Markdown(PHP Markdown Extra) text. 114func NewFootnoteParser() parser.InlineParser { 115 return defaultFootnoteParser 116} 117 118func (s *footnoteParser) Trigger() []byte { 119 // footnote syntax probably conflict with the image syntax. 120 // So we need trigger this parser with '!'. 121 return []byte{'!', '['} 122} 123 124func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { 125 line, segment := block.PeekLine() 126 pos := 1 127 if len(line) > 0 && line[0] == '!' { 128 pos++ 129 } 130 if pos >= len(line) || line[pos] != '^' { 131 return nil 132 } 133 pos++ 134 if pos >= len(line) { 135 return nil 136 } 137 open := pos 138 closure := util.FindClosure(line[pos:], '[', ']', false, false) 139 if closure < 0 { 140 return nil 141 } 142 closes := pos + closure 143 value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes)) 144 block.Advance(closes + 1) 145 146 var list *ast.FootnoteList 147 if tlist := pc.Get(footnoteListKey); tlist != nil { 148 list = tlist.(*ast.FootnoteList) 149 } 150 if list == nil { 151 return nil 152 } 153 index := 0 154 for def := list.FirstChild(); def != nil; def = def.NextSibling() { 155 d := def.(*ast.Footnote) 156 if bytes.Equal(d.Ref, value) { 157 if d.Index < 0 { 158 list.Count += 1 159 d.Index = list.Count 160 } 161 index = d.Index 162 break 163 } 164 } 165 if index == 0 { 166 return nil 167 } 168 169 fnlink := ast.NewFootnoteLink(index) 170 var fnlist []*ast.FootnoteLink 171 if tmp := pc.Get(footnoteLinkListKey); tmp != nil { 172 fnlist = tmp.([]*ast.FootnoteLink) 173 } else { 174 fnlist = []*ast.FootnoteLink{} 175 pc.Set(footnoteLinkListKey, fnlist) 176 } 177 pc.Set(footnoteLinkListKey, append(fnlist, fnlink)) 178 if line[0] == '!' { 179 parent.AppendChild(parent, gast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+1))) 180 } 181 182 return fnlink 183} 184 185type footnoteASTTransformer struct { 186} 187 188var defaultFootnoteASTTransformer = &footnoteASTTransformer{} 189 190// NewFootnoteASTTransformer returns a new parser.ASTTransformer that 191// insert a footnote list to the last of the document. 192func NewFootnoteASTTransformer() parser.ASTTransformer { 193 return defaultFootnoteASTTransformer 194} 195 196func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { 197 var list *ast.FootnoteList 198 var fnlist []*ast.FootnoteLink 199 if tmp := pc.Get(footnoteListKey); tmp != nil { 200 list = tmp.(*ast.FootnoteList) 201 } 202 if tmp := pc.Get(footnoteLinkListKey); tmp != nil { 203 fnlist = tmp.([]*ast.FootnoteLink) 204 } 205 206 pc.Set(footnoteListKey, nil) 207 pc.Set(footnoteLinkListKey, nil) 208 209 if list == nil { 210 return 211 } 212 213 counter := map[int]int{} 214 if fnlist != nil { 215 for _, fnlink := range fnlist { 216 if fnlink.Index >= 0 { 217 counter[fnlink.Index]++ 218 } 219 } 220 for _, fnlink := range fnlist { 221 fnlink.RefCount = counter[fnlink.Index] 222 } 223 } 224 for footnote := list.FirstChild(); footnote != nil; { 225 var container gast.Node = footnote 226 next := footnote.NextSibling() 227 if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) { 228 container = fc 229 } 230 fn := footnote.(*ast.Footnote) 231 index := fn.Index 232 if index < 0 { 233 list.RemoveChild(list, footnote) 234 } else { 235 backLink := ast.NewFootnoteBacklink(index) 236 backLink.RefCount = counter[index] 237 container.AppendChild(container, backLink) 238 } 239 footnote = next 240 } 241 list.SortChildren(func(n1, n2 gast.Node) int { 242 if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index { 243 return -1 244 } 245 return 1 246 }) 247 if list.Count <= 0 { 248 list.Parent().RemoveChild(list.Parent(), list) 249 return 250 } 251 252 node.AppendChild(node, list) 253} 254 255// FootnoteConfig holds configuration values for the footnote extension. 256// 257// Link* and Backlink* configurations have some variables: 258// Occurrances of “^^” in the string will be replaced by the 259// corresponding footnote number in the HTML output. 260// Occurrances of “%%” will be replaced by a number for the 261// reference (footnotes can have multiple references). 262type FootnoteConfig struct { 263 html.Config 264 265 // IDPrefix is a prefix for the id attributes generated by footnotes. 266 IDPrefix []byte 267 268 // IDPrefix is a function that determines the id attribute for given Node. 269 IDPrefixFunction func(gast.Node) []byte 270 271 // LinkTitle is an optional title attribute for footnote links. 272 LinkTitle []byte 273 274 // BacklinkTitle is an optional title attribute for footnote backlinks. 275 BacklinkTitle []byte 276 277 // LinkClass is a class for footnote links. 278 LinkClass []byte 279 280 // BacklinkClass is a class for footnote backlinks. 281 BacklinkClass []byte 282 283 // BacklinkHTML is an HTML content for footnote backlinks. 284 BacklinkHTML []byte 285} 286 287// FootnoteOption interface is a functional option interface for the extension. 288type FootnoteOption interface { 289 renderer.Option 290 // SetFootnoteOption sets given option to the extension. 291 SetFootnoteOption(*FootnoteConfig) 292} 293 294// NewFootnoteConfig returns a new Config with defaults. 295func NewFootnoteConfig() FootnoteConfig { 296 return FootnoteConfig{ 297 Config: html.NewConfig(), 298 LinkTitle: []byte(""), 299 BacklinkTitle: []byte(""), 300 LinkClass: []byte("footnote-ref"), 301 BacklinkClass: []byte("footnote-backref"), 302 BacklinkHTML: []byte("↩︎"), 303 } 304} 305 306// SetOption implements renderer.SetOptioner. 307func (c *FootnoteConfig) SetOption(name renderer.OptionName, value interface{}) { 308 switch name { 309 case optFootnoteIDPrefixFunction: 310 c.IDPrefixFunction = value.(func(gast.Node) []byte) 311 case optFootnoteIDPrefix: 312 c.IDPrefix = value.([]byte) 313 case optFootnoteLinkTitle: 314 c.LinkTitle = value.([]byte) 315 case optFootnoteBacklinkTitle: 316 c.BacklinkTitle = value.([]byte) 317 case optFootnoteLinkClass: 318 c.LinkClass = value.([]byte) 319 case optFootnoteBacklinkClass: 320 c.BacklinkClass = value.([]byte) 321 case optFootnoteBacklinkHTML: 322 c.BacklinkHTML = value.([]byte) 323 default: 324 c.Config.SetOption(name, value) 325 } 326} 327 328type withFootnoteHTMLOptions struct { 329 value []html.Option 330} 331 332func (o *withFootnoteHTMLOptions) SetConfig(c *renderer.Config) { 333 if o.value != nil { 334 for _, v := range o.value { 335 v.(renderer.Option).SetConfig(c) 336 } 337 } 338} 339 340func (o *withFootnoteHTMLOptions) SetFootnoteOption(c *FootnoteConfig) { 341 if o.value != nil { 342 for _, v := range o.value { 343 v.SetHTMLOption(&c.Config) 344 } 345 } 346} 347 348// WithFootnoteHTMLOptions is functional option that wraps goldmark HTMLRenderer options. 349func WithFootnoteHTMLOptions(opts ...html.Option) FootnoteOption { 350 return &withFootnoteHTMLOptions{opts} 351} 352 353const optFootnoteIDPrefix renderer.OptionName = "FootnoteIDPrefix" 354 355type withFootnoteIDPrefix struct { 356 value []byte 357} 358 359func (o *withFootnoteIDPrefix) SetConfig(c *renderer.Config) { 360 c.Options[optFootnoteIDPrefix] = o.value 361} 362 363func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) { 364 c.IDPrefix = o.value 365} 366 367// WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes. 368func WithFootnoteIDPrefix(a []byte) FootnoteOption { 369 return &withFootnoteIDPrefix{a} 370} 371 372const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction" 373 374type withFootnoteIDPrefixFunction struct { 375 value func(gast.Node) []byte 376} 377 378func (o *withFootnoteIDPrefixFunction) SetConfig(c *renderer.Config) { 379 c.Options[optFootnoteIDPrefixFunction] = o.value 380} 381 382func (o *withFootnoteIDPrefixFunction) SetFootnoteOption(c *FootnoteConfig) { 383 c.IDPrefixFunction = o.value 384} 385 386// WithFootnoteIDPrefixFunction is a functional option that is a prefix for the id attributes generated by footnotes. 387func WithFootnoteIDPrefixFunction(a func(gast.Node) []byte) FootnoteOption { 388 return &withFootnoteIDPrefixFunction{a} 389} 390 391const optFootnoteLinkTitle renderer.OptionName = "FootnoteLinkTitle" 392 393type withFootnoteLinkTitle struct { 394 value []byte 395} 396 397func (o *withFootnoteLinkTitle) SetConfig(c *renderer.Config) { 398 c.Options[optFootnoteLinkTitle] = o.value 399} 400 401func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) { 402 c.LinkTitle = o.value 403} 404 405// WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links. 406func WithFootnoteLinkTitle(a []byte) FootnoteOption { 407 return &withFootnoteLinkTitle{a} 408} 409 410const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle" 411 412type withFootnoteBacklinkTitle struct { 413 value []byte 414} 415 416func (o *withFootnoteBacklinkTitle) SetConfig(c *renderer.Config) { 417 c.Options[optFootnoteBacklinkTitle] = o.value 418} 419 420func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) { 421 c.BacklinkTitle = o.value 422} 423 424// WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks. 425func WithFootnoteBacklinkTitle(a []byte) FootnoteOption { 426 return &withFootnoteBacklinkTitle{a} 427} 428 429const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass" 430 431type withFootnoteLinkClass struct { 432 value []byte 433} 434 435func (o *withFootnoteLinkClass) SetConfig(c *renderer.Config) { 436 c.Options[optFootnoteLinkClass] = o.value 437} 438 439func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) { 440 c.LinkClass = o.value 441} 442 443// WithFootnoteLinkClass is a functional option that is a class for footnote links. 444func WithFootnoteLinkClass(a []byte) FootnoteOption { 445 return &withFootnoteLinkClass{a} 446} 447 448const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass" 449 450type withFootnoteBacklinkClass struct { 451 value []byte 452} 453 454func (o *withFootnoteBacklinkClass) SetConfig(c *renderer.Config) { 455 c.Options[optFootnoteBacklinkClass] = o.value 456} 457 458func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) { 459 c.BacklinkClass = o.value 460} 461 462// WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks. 463func WithFootnoteBacklinkClass(a []byte) FootnoteOption { 464 return &withFootnoteBacklinkClass{a} 465} 466 467const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML" 468 469type withFootnoteBacklinkHTML struct { 470 value []byte 471} 472 473func (o *withFootnoteBacklinkHTML) SetConfig(c *renderer.Config) { 474 c.Options[optFootnoteBacklinkHTML] = o.value 475} 476 477func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) { 478 c.BacklinkHTML = o.value 479} 480 481// WithFootnoteBacklinkHTML is an HTML content for footnote backlinks. 482func WithFootnoteBacklinkHTML(a []byte) FootnoteOption { 483 return &withFootnoteBacklinkHTML{a} 484} 485 486// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that 487// renders FootnoteLink nodes. 488type FootnoteHTMLRenderer struct { 489 FootnoteConfig 490} 491 492// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. 493func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer { 494 r := &FootnoteHTMLRenderer{ 495 FootnoteConfig: NewFootnoteConfig(), 496 } 497 for _, opt := range opts { 498 opt.SetFootnoteOption(&r.FootnoteConfig) 499 } 500 return r 501} 502 503// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 504func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 505 reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink) 506 reg.Register(ast.KindFootnoteBacklink, r.renderFootnoteBacklink) 507 reg.Register(ast.KindFootnote, r.renderFootnote) 508 reg.Register(ast.KindFootnoteList, r.renderFootnoteList) 509} 510 511func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 512 if entering { 513 n := node.(*ast.FootnoteLink) 514 is := strconv.Itoa(n.Index) 515 _, _ = w.WriteString(`<sup id="`) 516 _, _ = w.Write(r.idPrefix(node)) 517 _, _ = w.WriteString(`fnref:`) 518 _, _ = w.WriteString(is) 519 _, _ = w.WriteString(`"><a href="#`) 520 _, _ = w.Write(r.idPrefix(node)) 521 _, _ = w.WriteString(`fn:`) 522 _, _ = w.WriteString(is) 523 _, _ = w.WriteString(`" class="`) 524 _, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.LinkClass, 525 n.Index, n.RefCount)) 526 if len(r.FootnoteConfig.LinkTitle) > 0 { 527 _, _ = w.WriteString(`" title="`) 528 _, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.LinkTitle, n.Index, n.RefCount))) 529 } 530 _, _ = w.WriteString(`" role="doc-noteref">`) 531 532 _, _ = w.WriteString(is) 533 _, _ = w.WriteString(`</a></sup>`) 534 } 535 return gast.WalkContinue, nil 536} 537 538func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 539 if entering { 540 n := node.(*ast.FootnoteBacklink) 541 is := strconv.Itoa(n.Index) 542 _, _ = w.WriteString(` <a href="#`) 543 _, _ = w.Write(r.idPrefix(node)) 544 _, _ = w.WriteString(`fnref:`) 545 _, _ = w.WriteString(is) 546 _, _ = w.WriteString(`" class="`) 547 _, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkClass, n.Index, n.RefCount)) 548 if len(r.FootnoteConfig.BacklinkTitle) > 0 { 549 _, _ = w.WriteString(`" title="`) 550 _, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.BacklinkTitle, n.Index, n.RefCount))) 551 } 552 _, _ = w.WriteString(`" role="doc-backlink">`) 553 _, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, n.Index, n.RefCount)) 554 _, _ = w.WriteString(`</a>`) 555 } 556 return gast.WalkContinue, nil 557} 558 559func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 560 n := node.(*ast.Footnote) 561 is := strconv.Itoa(n.Index) 562 if entering { 563 _, _ = w.WriteString(`<li id="`) 564 _, _ = w.Write(r.idPrefix(node)) 565 _, _ = w.WriteString(`fn:`) 566 _, _ = w.WriteString(is) 567 _, _ = w.WriteString(`" role="doc-endnote"`) 568 if node.Attributes() != nil { 569 html.RenderAttributes(w, node, html.ListItemAttributeFilter) 570 } 571 _, _ = w.WriteString(">\n") 572 } else { 573 _, _ = w.WriteString("</li>\n") 574 } 575 return gast.WalkContinue, nil 576} 577 578func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 579 tag := "section" 580 if r.Config.XHTML { 581 tag = "div" 582 } 583 if entering { 584 _, _ = w.WriteString("<") 585 _, _ = w.WriteString(tag) 586 _, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`) 587 if node.Attributes() != nil { 588 html.RenderAttributes(w, node, html.GlobalAttributeFilter) 589 } 590 _ = w.WriteByte('>') 591 if r.Config.XHTML { 592 _, _ = w.WriteString("\n<hr />\n") 593 } else { 594 _, _ = w.WriteString("\n<hr>\n") 595 } 596 _, _ = w.WriteString("<ol>\n") 597 } else { 598 _, _ = w.WriteString("</ol>\n") 599 _, _ = w.WriteString("</") 600 _, _ = w.WriteString(tag) 601 _, _ = w.WriteString(">\n") 602 } 603 return gast.WalkContinue, nil 604} 605 606func (r *FootnoteHTMLRenderer) idPrefix(node gast.Node) []byte { 607 if r.FootnoteConfig.IDPrefix != nil { 608 return r.FootnoteConfig.IDPrefix 609 } 610 if r.FootnoteConfig.IDPrefixFunction != nil { 611 return r.FootnoteConfig.IDPrefixFunction(node) 612 } 613 return []byte("") 614} 615 616func applyFootnoteTemplate(b []byte, index, refCount int) []byte { 617 fast := true 618 for i, c := range b { 619 if i != 0 { 620 if b[i-1] == '^' && c == '^' { 621 fast = false 622 break 623 } 624 if b[i-1] == '%' && c == '%' { 625 fast = false 626 break 627 } 628 } 629 } 630 if fast { 631 return b 632 } 633 is := []byte(strconv.Itoa(index)) 634 rs := []byte(strconv.Itoa(refCount)) 635 ret := bytes.Replace(b, []byte("^^"), is, -1) 636 return bytes.Replace(ret, []byte("%%"), rs, -1) 637} 638 639type footnote struct { 640 options []FootnoteOption 641} 642 643// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes. 644var Footnote = &footnote{ 645 options: []FootnoteOption{}, 646} 647 648// NewFootnote returns a new extension with given options. 649func NewFootnote(opts ...FootnoteOption) goldmark.Extender { 650 return &footnote{ 651 options: opts, 652 } 653} 654 655func (e *footnote) Extend(m goldmark.Markdown) { 656 m.Parser().AddOptions( 657 parser.WithBlockParsers( 658 util.Prioritized(NewFootnoteBlockParser(), 999), 659 ), 660 parser.WithInlineParsers( 661 util.Prioritized(NewFootnoteParser(), 101), 662 ), 663 parser.WithASTTransformers( 664 util.Prioritized(NewFootnoteASTTransformer(), 999), 665 ), 666 ) 667 m.Renderer().AddOptions(renderer.WithNodeRenderers( 668 util.Prioritized(NewFootnoteHTMLRenderer(e.options...), 500), 669 )) 670} 671