footnote.go

  1package extension
  2
  3import (
  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
 18var footnoteListKey = parser.NewContextKey()
 19var footnoteLinkListKey = parser.NewContextKey()
 20
 21type footnoteBlockParser struct {
 22}
 23
 24var defaultFootnoteBlockParser = &footnoteBlockParser{}
 25
 26// NewFootnoteBlockParser returns a new parser.BlockParser that can parse
 27// footnotes of the Markdown(PHP Markdown Extra) text.
 28func NewFootnoteBlockParser() parser.BlockParser {
 29	return defaultFootnoteBlockParser
 30}
 31
 32func (b *footnoteBlockParser) Trigger() []byte {
 33	return []byte{'['}
 34}
 35
 36func (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	var closes int
 48	closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint:staticcheck
 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
 74func (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
 87func (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
100func (b *footnoteBlockParser) CanInterruptParagraph() bool {
101	return true
102}
103
104func (b *footnoteBlockParser) CanAcceptIndentedLine() bool {
105	return false
106}
107
108type footnoteParser struct {
109}
110
111var defaultFootnoteParser = &footnoteParser{}
112
113// NewFootnoteParser returns a new parser.InlineParser that can parse
114// footnote links of the Markdown(PHP Markdown Extra) text.
115func NewFootnoteParser() parser.InlineParser {
116	return defaultFootnoteParser
117}
118
119func (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
125func (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) //nolint:staticcheck
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++
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
186type footnoteASTTransformer struct {
187}
188
189var defaultFootnoteASTTransformer = &footnoteASTTransformer{}
190
191// NewFootnoteASTTransformer returns a new parser.ASTTransformer that
192// insert a footnote list to the last of the document.
193func NewFootnoteASTTransformer() parser.ASTTransformer {
194	return defaultFootnoteASTTransformer
195}
196
197func (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// Occurrences of “^^” in the string will be replaced by the
276// corresponding footnote number in the HTML output.
277// Occurrences of “%%” will be replaced by a number for the
278// reference (footnotes can have multiple references).
279type 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.
305type 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.
312func 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("&#x21a9;&#xfe0e;"),
320	}
321}
322
323// SetOption implements renderer.SetOptioner.
324func (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
345type withFootnoteHTMLOptions struct {
346	value []html.Option
347}
348
349func (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
357func (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.
366func WithFootnoteHTMLOptions(opts ...html.Option) FootnoteOption {
367	return &withFootnoteHTMLOptions{opts}
368}
369
370const optFootnoteIDPrefix renderer.OptionName = "FootnoteIDPrefix"
371
372type withFootnoteIDPrefix struct {
373	value []byte
374}
375
376func (o *withFootnoteIDPrefix) SetConfig(c *renderer.Config) {
377	c.Options[optFootnoteIDPrefix] = o.value
378}
379
380func (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.
385func WithFootnoteIDPrefix[T []byte | string](a T) FootnoteOption {
386	return &withFootnoteIDPrefix{[]byte(a)}
387}
388
389const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction"
390
391type withFootnoteIDPrefixFunction struct {
392	value func(gast.Node) []byte
393}
394
395func (o *withFootnoteIDPrefixFunction) SetConfig(c *renderer.Config) {
396	c.Options[optFootnoteIDPrefixFunction] = o.value
397}
398
399func (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.
404func WithFootnoteIDPrefixFunction(a func(gast.Node) []byte) FootnoteOption {
405	return &withFootnoteIDPrefixFunction{a}
406}
407
408const optFootnoteLinkTitle renderer.OptionName = "FootnoteLinkTitle"
409
410type withFootnoteLinkTitle struct {
411	value []byte
412}
413
414func (o *withFootnoteLinkTitle) SetConfig(c *renderer.Config) {
415	c.Options[optFootnoteLinkTitle] = o.value
416}
417
418func (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.
423func WithFootnoteLinkTitle[T []byte | string](a T) FootnoteOption {
424	return &withFootnoteLinkTitle{[]byte(a)}
425}
426
427const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle"
428
429type withFootnoteBacklinkTitle struct {
430	value []byte
431}
432
433func (o *withFootnoteBacklinkTitle) SetConfig(c *renderer.Config) {
434	c.Options[optFootnoteBacklinkTitle] = o.value
435}
436
437func (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.
442func WithFootnoteBacklinkTitle[T []byte | string](a T) FootnoteOption {
443	return &withFootnoteBacklinkTitle{[]byte(a)}
444}
445
446const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass"
447
448type withFootnoteLinkClass struct {
449	value []byte
450}
451
452func (o *withFootnoteLinkClass) SetConfig(c *renderer.Config) {
453	c.Options[optFootnoteLinkClass] = o.value
454}
455
456func (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.
461func WithFootnoteLinkClass[T []byte | string](a T) FootnoteOption {
462	return &withFootnoteLinkClass{[]byte(a)}
463}
464
465const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass"
466
467type withFootnoteBacklinkClass struct {
468	value []byte
469}
470
471func (o *withFootnoteBacklinkClass) SetConfig(c *renderer.Config) {
472	c.Options[optFootnoteBacklinkClass] = o.value
473}
474
475func (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.
480func WithFootnoteBacklinkClass[T []byte | string](a T) FootnoteOption {
481	return &withFootnoteBacklinkClass{[]byte(a)}
482}
483
484const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML"
485
486type withFootnoteBacklinkHTML struct {
487	value []byte
488}
489
490func (o *withFootnoteBacklinkHTML) SetConfig(c *renderer.Config) {
491	c.Options[optFootnoteBacklinkHTML] = o.value
492}
493
494func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) {
495	c.BacklinkHTML = o.value
496}
497
498// WithFootnoteBacklinkHTML is an HTML content for footnote backlinks.
499func WithFootnoteBacklinkHTML[T []byte | string](a T) FootnoteOption {
500	return &withFootnoteBacklinkHTML{[]byte(a)}
501}
502
503// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
504// renders FootnoteLink nodes.
505type FootnoteHTMLRenderer struct {
506	FootnoteConfig
507}
508
509// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
510func 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.
521func (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
528func (r *FootnoteHTMLRenderer) renderFootnoteLink(
529	w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
530	if entering {
531		n := node.(*ast.FootnoteLink)
532		is := strconv.Itoa(n.Index)
533		_, _ = w.WriteString(`<sup id="`)
534		_, _ = w.Write(r.idPrefix(node))
535		_, _ = w.WriteString(`fnref`)
536		if n.RefIndex > 0 {
537			_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex))
538		}
539		_ = w.WriteByte(':')
540		_, _ = w.WriteString(is)
541		_, _ = w.WriteString(`"><a href="#`)
542		_, _ = w.Write(r.idPrefix(node))
543		_, _ = w.WriteString(`fn:`)
544		_, _ = w.WriteString(is)
545		_, _ = w.WriteString(`" class="`)
546		_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.LinkClass,
547			n.Index, n.RefCount))
548		if len(r.FootnoteConfig.LinkTitle) > 0 {
549			_, _ = w.WriteString(`" title="`)
550			_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.LinkTitle, n.Index, n.RefCount)))
551		}
552		_, _ = w.WriteString(`" role="doc-noteref">`)
553
554		_, _ = w.WriteString(is)
555		_, _ = w.WriteString(`</a></sup>`)
556	}
557	return gast.WalkContinue, nil
558}
559
560func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(
561	w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
562	if entering {
563		n := node.(*ast.FootnoteBacklink)
564		is := strconv.Itoa(n.Index)
565		_, _ = w.WriteString(`&#160;<a href="#`)
566		_, _ = w.Write(r.idPrefix(node))
567		_, _ = w.WriteString(`fnref`)
568		if n.RefIndex > 0 {
569			_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex))
570		}
571		_ = w.WriteByte(':')
572		_, _ = w.WriteString(is)
573		_, _ = w.WriteString(`" class="`)
574		_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkClass, n.Index, n.RefCount))
575		if len(r.FootnoteConfig.BacklinkTitle) > 0 {
576			_, _ = w.WriteString(`" title="`)
577			_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.BacklinkTitle, n.Index, n.RefCount)))
578		}
579		_, _ = w.WriteString(`" role="doc-backlink">`)
580		_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, n.Index, n.RefCount))
581		_, _ = w.WriteString(`</a>`)
582	}
583	return gast.WalkContinue, nil
584}
585
586func (r *FootnoteHTMLRenderer) renderFootnote(
587	w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
588	n := node.(*ast.Footnote)
589	is := strconv.Itoa(n.Index)
590	if entering {
591		_, _ = w.WriteString(`<li id="`)
592		_, _ = w.Write(r.idPrefix(node))
593		_, _ = w.WriteString(`fn:`)
594		_, _ = w.WriteString(is)
595		_, _ = w.WriteString(`"`)
596		if node.Attributes() != nil {
597			html.RenderAttributes(w, node, html.ListItemAttributeFilter)
598		}
599		_, _ = w.WriteString(">\n")
600	} else {
601		_, _ = w.WriteString("</li>\n")
602	}
603	return gast.WalkContinue, nil
604}
605
606func (r *FootnoteHTMLRenderer) renderFootnoteList(
607	w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
608	if entering {
609		_, _ = w.WriteString(`<div class="footnotes" role="doc-endnotes"`)
610		if node.Attributes() != nil {
611			html.RenderAttributes(w, node, html.GlobalAttributeFilter)
612		}
613		_ = w.WriteByte('>')
614		if r.Config.XHTML {
615			_, _ = w.WriteString("\n<hr />\n")
616		} else {
617			_, _ = w.WriteString("\n<hr>\n")
618		}
619		_, _ = w.WriteString("<ol>\n")
620	} else {
621		_, _ = w.WriteString("</ol>\n")
622		_, _ = w.WriteString("</div>\n")
623	}
624	return gast.WalkContinue, nil
625}
626
627func (r *FootnoteHTMLRenderer) idPrefix(node gast.Node) []byte {
628	if r.FootnoteConfig.IDPrefix != nil {
629		return r.FootnoteConfig.IDPrefix
630	}
631	if r.FootnoteConfig.IDPrefixFunction != nil {
632		return r.FootnoteConfig.IDPrefixFunction(node)
633	}
634	return []byte("")
635}
636
637func applyFootnoteTemplate(b []byte, index, refCount int) []byte {
638	fast := true
639	for i, c := range b {
640		if i != 0 {
641			if b[i-1] == '^' && c == '^' {
642				fast = false
643				break
644			}
645			if b[i-1] == '%' && c == '%' {
646				fast = false
647				break
648			}
649		}
650	}
651	if fast {
652		return b
653	}
654	is := []byte(strconv.Itoa(index))
655	rs := []byte(strconv.Itoa(refCount))
656	ret := bytes.Replace(b, []byte("^^"), is, -1)
657	return bytes.Replace(ret, []byte("%%"), rs, -1)
658}
659
660type footnote struct {
661	options []FootnoteOption
662}
663
664// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes.
665var Footnote = &footnote{
666	options: []FootnoteOption{},
667}
668
669// NewFootnote returns a new extension with given options.
670func NewFootnote(opts ...FootnoteOption) goldmark.Extender {
671	return &footnote{
672		options: opts,
673	}
674}
675
676func (e *footnote) Extend(m goldmark.Markdown) {
677	m.Parser().AddOptions(
678		parser.WithBlockParsers(
679			util.Prioritized(NewFootnoteBlockParser(), 999),
680		),
681		parser.WithInlineParsers(
682			util.Prioritized(NewFootnoteParser(), 101),
683		),
684		parser.WithASTTransformers(
685			util.Prioritized(NewFootnoteASTTransformer(), 999),
686		),
687	)
688	m.Renderer().AddOptions(renderer.WithNodeRenderers(
689		util.Prioritized(NewFootnoteHTMLRenderer(e.options...), 500),
690	))
691}