package extension import ( "bytes" "fmt" "strconv" "github.com/yuin/goldmark" gast "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) var footnoteListKey = parser.NewContextKey() var footnoteLinkListKey = parser.NewContextKey() type footnoteBlockParser struct { } var defaultFootnoteBlockParser = &footnoteBlockParser{} // NewFootnoteBlockParser returns a new parser.BlockParser that can parse // footnotes of the Markdown(PHP Markdown Extra) text. func NewFootnoteBlockParser() parser.BlockParser { return defaultFootnoteBlockParser } func (b *footnoteBlockParser) Trigger() []byte { return []byte{'['} } func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { line, segment := reader.PeekLine() pos := pc.BlockOffset() if pos < 0 || line[pos] != '[' { return nil, parser.NoChildren } pos++ if pos > len(line)-1 || line[pos] != '^' { return nil, parser.NoChildren } open := pos + 1 closes := 0 closure := util.FindClosure(line[pos+1:], '[', ']', false, false) closes = pos + 1 + closure next := closes + 1 if closure > -1 { if next >= len(line) || line[next] != ':' { return nil, parser.NoChildren } } else { return nil, parser.NoChildren } padding := segment.Padding label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding)) if util.IsBlank(label) { return nil, parser.NoChildren } item := ast.NewFootnote(label) pos = next + 1 - padding if pos >= len(line) { reader.Advance(pos) return item, parser.NoChildren } reader.AdvanceAndSetPadding(pos, padding) return item, parser.HasChildren } func (b *footnoteBlockParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { line, _ := reader.PeekLine() if util.IsBlank(line) { return parser.Continue | parser.HasChildren } childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4) if childpos < 0 { return parser.Close } reader.AdvanceAndSetPadding(childpos, padding) return parser.Continue | parser.HasChildren } func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { var list *ast.FootnoteList if tlist := pc.Get(footnoteListKey); tlist != nil { list = tlist.(*ast.FootnoteList) } else { list = ast.NewFootnoteList() pc.Set(footnoteListKey, list) node.Parent().InsertBefore(node.Parent(), node, list) } node.Parent().RemoveChild(node.Parent(), node) list.AppendChild(list, node) } func (b *footnoteBlockParser) CanInterruptParagraph() bool { return true } func (b *footnoteBlockParser) CanAcceptIndentedLine() bool { return false } type footnoteParser struct { } var defaultFootnoteParser = &footnoteParser{} // NewFootnoteParser returns a new parser.InlineParser that can parse // footnote links of the Markdown(PHP Markdown Extra) text. func NewFootnoteParser() parser.InlineParser { return defaultFootnoteParser } func (s *footnoteParser) Trigger() []byte { // footnote syntax probably conflict with the image syntax. // So we need trigger this parser with '!'. return []byte{'!', '['} } func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { line, segment := block.PeekLine() pos := 1 if len(line) > 0 && line[0] == '!' { pos++ } if pos >= len(line) || line[pos] != '^' { return nil } pos++ if pos >= len(line) { return nil } open := pos closure := util.FindClosure(line[pos:], '[', ']', false, false) if closure < 0 { return nil } closes := pos + closure value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes)) block.Advance(closes + 1) var list *ast.FootnoteList if tlist := pc.Get(footnoteListKey); tlist != nil { list = tlist.(*ast.FootnoteList) } if list == nil { return nil } index := 0 for def := list.FirstChild(); def != nil; def = def.NextSibling() { d := def.(*ast.Footnote) if bytes.Equal(d.Ref, value) { if d.Index < 0 { list.Count += 1 d.Index = list.Count } index = d.Index break } } if index == 0 { return nil } fnlink := ast.NewFootnoteLink(index) var fnlist []*ast.FootnoteLink if tmp := pc.Get(footnoteLinkListKey); tmp != nil { fnlist = tmp.([]*ast.FootnoteLink) } else { fnlist = []*ast.FootnoteLink{} pc.Set(footnoteLinkListKey, fnlist) } pc.Set(footnoteLinkListKey, append(fnlist, fnlink)) if line[0] == '!' { parent.AppendChild(parent, gast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+1))) } return fnlink } type footnoteASTTransformer struct { } var defaultFootnoteASTTransformer = &footnoteASTTransformer{} // NewFootnoteASTTransformer returns a new parser.ASTTransformer that // insert a footnote list to the last of the document. func NewFootnoteASTTransformer() parser.ASTTransformer { return defaultFootnoteASTTransformer } func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { var list *ast.FootnoteList var fnlist []*ast.FootnoteLink if tmp := pc.Get(footnoteListKey); tmp != nil { list = tmp.(*ast.FootnoteList) } if tmp := pc.Get(footnoteLinkListKey); tmp != nil { fnlist = tmp.([]*ast.FootnoteLink) } pc.Set(footnoteListKey, nil) pc.Set(footnoteLinkListKey, nil) if list == nil { return } counter := map[int]int{} if fnlist != nil { for _, fnlink := range fnlist { if fnlink.Index >= 0 { counter[fnlink.Index]++ } } refCounter := map[int]int{} for _, fnlink := range fnlist { fnlink.RefCount = counter[fnlink.Index] if _, ok := refCounter[fnlink.Index]; !ok { refCounter[fnlink.Index] = 0 } fnlink.RefIndex = refCounter[fnlink.Index] refCounter[fnlink.Index]++ } } for footnote := list.FirstChild(); footnote != nil; { var container gast.Node = footnote next := footnote.NextSibling() if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) { container = fc } fn := footnote.(*ast.Footnote) index := fn.Index if index < 0 { list.RemoveChild(list, footnote) } else { refCount := counter[index] backLink := ast.NewFootnoteBacklink(index) backLink.RefCount = refCount backLink.RefIndex = 0 container.AppendChild(container, backLink) if refCount > 1 { for i := 1; i < refCount; i++ { backLink := ast.NewFootnoteBacklink(index) backLink.RefCount = refCount backLink.RefIndex = i container.AppendChild(container, backLink) } } } footnote = next } list.SortChildren(func(n1, n2 gast.Node) int { if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index { return -1 } return 1 }) if list.Count <= 0 { list.Parent().RemoveChild(list.Parent(), list) return } node.AppendChild(node, list) } // FootnoteConfig holds configuration values for the footnote extension. // // Link* and Backlink* configurations have some variables: // Occurrances of “^^” in the string will be replaced by the // corresponding footnote number in the HTML output. // Occurrances of “%%” will be replaced by a number for the // reference (footnotes can have multiple references). type FootnoteConfig struct { html.Config // IDPrefix is a prefix for the id attributes generated by footnotes. IDPrefix []byte // IDPrefix is a function that determines the id attribute for given Node. IDPrefixFunction func(gast.Node) []byte // LinkTitle is an optional title attribute for footnote links. LinkTitle []byte // BacklinkTitle is an optional title attribute for footnote backlinks. BacklinkTitle []byte // LinkClass is a class for footnote links. LinkClass []byte // BacklinkClass is a class for footnote backlinks. BacklinkClass []byte // BacklinkHTML is an HTML content for footnote backlinks. BacklinkHTML []byte } // FootnoteOption interface is a functional option interface for the extension. type FootnoteOption interface { renderer.Option // SetFootnoteOption sets given option to the extension. SetFootnoteOption(*FootnoteConfig) } // NewFootnoteConfig returns a new Config with defaults. func NewFootnoteConfig() FootnoteConfig { return FootnoteConfig{ Config: html.NewConfig(), LinkTitle: []byte(""), BacklinkTitle: []byte(""), LinkClass: []byte("footnote-ref"), BacklinkClass: []byte("footnote-backref"), BacklinkHTML: []byte("↩︎"), } } // SetOption implements renderer.SetOptioner. func (c *FootnoteConfig) SetOption(name renderer.OptionName, value interface{}) { switch name { case optFootnoteIDPrefixFunction: c.IDPrefixFunction = value.(func(gast.Node) []byte) case optFootnoteIDPrefix: c.IDPrefix = value.([]byte) case optFootnoteLinkTitle: c.LinkTitle = value.([]byte) case optFootnoteBacklinkTitle: c.BacklinkTitle = value.([]byte) case optFootnoteLinkClass: c.LinkClass = value.([]byte) case optFootnoteBacklinkClass: c.BacklinkClass = value.([]byte) case optFootnoteBacklinkHTML: c.BacklinkHTML = value.([]byte) default: c.Config.SetOption(name, value) } } type withFootnoteHTMLOptions struct { value []html.Option } func (o *withFootnoteHTMLOptions) SetConfig(c *renderer.Config) { if o.value != nil { for _, v := range o.value { v.(renderer.Option).SetConfig(c) } } } func (o *withFootnoteHTMLOptions) SetFootnoteOption(c *FootnoteConfig) { if o.value != nil { for _, v := range o.value { v.SetHTMLOption(&c.Config) } } } // WithFootnoteHTMLOptions is functional option that wraps goldmark HTMLRenderer options. func WithFootnoteHTMLOptions(opts ...html.Option) FootnoteOption { return &withFootnoteHTMLOptions{opts} } const optFootnoteIDPrefix renderer.OptionName = "FootnoteIDPrefix" type withFootnoteIDPrefix struct { value []byte } func (o *withFootnoteIDPrefix) SetConfig(c *renderer.Config) { c.Options[optFootnoteIDPrefix] = o.value } func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) { c.IDPrefix = o.value } // WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes. func WithFootnoteIDPrefix(a []byte) FootnoteOption { return &withFootnoteIDPrefix{a} } const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction" type withFootnoteIDPrefixFunction struct { value func(gast.Node) []byte } func (o *withFootnoteIDPrefixFunction) SetConfig(c *renderer.Config) { c.Options[optFootnoteIDPrefixFunction] = o.value } func (o *withFootnoteIDPrefixFunction) SetFootnoteOption(c *FootnoteConfig) { c.IDPrefixFunction = o.value } // WithFootnoteIDPrefixFunction is a functional option that is a prefix for the id attributes generated by footnotes. func WithFootnoteIDPrefixFunction(a func(gast.Node) []byte) FootnoteOption { return &withFootnoteIDPrefixFunction{a} } const optFootnoteLinkTitle renderer.OptionName = "FootnoteLinkTitle" type withFootnoteLinkTitle struct { value []byte } func (o *withFootnoteLinkTitle) SetConfig(c *renderer.Config) { c.Options[optFootnoteLinkTitle] = o.value } func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) { c.LinkTitle = o.value } // WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links. func WithFootnoteLinkTitle(a []byte) FootnoteOption { return &withFootnoteLinkTitle{a} } const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle" type withFootnoteBacklinkTitle struct { value []byte } func (o *withFootnoteBacklinkTitle) SetConfig(c *renderer.Config) { c.Options[optFootnoteBacklinkTitle] = o.value } func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) { c.BacklinkTitle = o.value } // WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks. func WithFootnoteBacklinkTitle(a []byte) FootnoteOption { return &withFootnoteBacklinkTitle{a} } const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass" type withFootnoteLinkClass struct { value []byte } func (o *withFootnoteLinkClass) SetConfig(c *renderer.Config) { c.Options[optFootnoteLinkClass] = o.value } func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) { c.LinkClass = o.value } // WithFootnoteLinkClass is a functional option that is a class for footnote links. func WithFootnoteLinkClass(a []byte) FootnoteOption { return &withFootnoteLinkClass{a} } const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass" type withFootnoteBacklinkClass struct { value []byte } func (o *withFootnoteBacklinkClass) SetConfig(c *renderer.Config) { c.Options[optFootnoteBacklinkClass] = o.value } func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) { c.BacklinkClass = o.value } // WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks. func WithFootnoteBacklinkClass(a []byte) FootnoteOption { return &withFootnoteBacklinkClass{a} } const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML" type withFootnoteBacklinkHTML struct { value []byte } func (o *withFootnoteBacklinkHTML) SetConfig(c *renderer.Config) { c.Options[optFootnoteBacklinkHTML] = o.value } func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) { c.BacklinkHTML = o.value } // WithFootnoteBacklinkHTML is an HTML content for footnote backlinks. func WithFootnoteBacklinkHTML(a []byte) FootnoteOption { return &withFootnoteBacklinkHTML{a} } // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that // renders FootnoteLink nodes. type FootnoteHTMLRenderer struct { FootnoteConfig } // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer { r := &FootnoteHTMLRenderer{ FootnoteConfig: NewFootnoteConfig(), } for _, opt := range opts { opt.SetFootnoteOption(&r.FootnoteConfig) } return r } // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink) reg.Register(ast.KindFootnoteBacklink, r.renderFootnoteBacklink) reg.Register(ast.KindFootnote, r.renderFootnote) reg.Register(ast.KindFootnoteList, r.renderFootnoteList) } func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { if entering { n := node.(*ast.FootnoteLink) is := strconv.Itoa(n.Index) _, _ = w.WriteString(``) _, _ = w.WriteString(is) _, _ = w.WriteString(``) } return gast.WalkContinue, nil } func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { if entering { n := node.(*ast.FootnoteBacklink) is := strconv.Itoa(n.Index) _, _ = w.WriteString(` `) _, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, n.Index, n.RefCount)) _, _ = w.WriteString(``) } return gast.WalkContinue, nil } func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { n := node.(*ast.Footnote) is := strconv.Itoa(n.Index) if entering { _, _ = w.WriteString(`