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 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
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 += 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
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(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
661 var Footnote = &footnote{
662 options: []FootnoteOption{},
663 }
664
665
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 }
688
View as plain text