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