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
27
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 var closes int
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
114
115 func NewFootnoteParser() parser.InlineParser {
116 return defaultFootnoteParser
117 }
118
119 func (s *footnoteParser) Trigger() []byte {
120
121
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++
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
192
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
273
274
275
276
277
278
279 type FootnoteConfig struct {
280 html.Config
281
282
283 IDPrefix []byte
284
285
286 IDPrefixFunction func(gast.Node) []byte
287
288
289 LinkTitle []byte
290
291
292 BacklinkTitle []byte
293
294
295 LinkClass []byte
296
297
298 BacklinkClass []byte
299
300
301 BacklinkHTML []byte
302 }
303
304
305 type FootnoteOption interface {
306 renderer.Option
307
308 SetFootnoteOption(*FootnoteConfig)
309 }
310
311
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
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
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
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
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
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
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
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
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
499 func WithFootnoteBacklinkHTML(a []byte) FootnoteOption {
500 return &withFootnoteBacklinkHTML{a}
501 }
502
503
504
505 type FootnoteHTMLRenderer struct {
506 FootnoteConfig
507 }
508
509
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
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(
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
560 func (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(` <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
586 func (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
606 func (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
627 func (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
637 func 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
660 type footnote struct {
661 options []FootnoteOption
662 }
663
664
665 var Footnote = &footnote{
666 options: []FootnoteOption{},
667 }
668
669
670 func NewFootnote(opts ...FootnoteOption) goldmark.Extender {
671 return &footnote{
672 options: opts,
673 }
674 }
675
676 func (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 }
692
View as plain text