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("&#x21a9;&#xfe0e;"),
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(`&#160;<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