From 9e0189df270cb443c204b51e89e2b0749b98370c Mon Sep 17 00:00:00 2001
From: yuin
Date: Sun, 13 Dec 2020 23:11:07 +0900
Subject: [PATCH] Closes #161
- Implement footnote configurations defined in original markdown extra.
- Add OwnerDocument() method to ast.Node
- Add Meta() method to *ast.Document
---
.github/ISSUE_TEMPLATE.md | 8 +-
README.md | 83 ++++++++
ast/ast.go | 21 ++
ast/block.go | 21 ++
extension/ast/footnote.go | 35 ++--
extension/footnote.go | 379 ++++++++++++++++++++++++++++++++++---
extension/footnote_test.go | 122 ++++++++++++
testutil/testutil.go | 9 +-
8 files changed, 632 insertions(+), 46 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 47e7aaa..51e3b62 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,7 +1,10 @@
+goldmark has [https://github.com/yuin/goldmark/discussions](Discussions) in github.
+You should post only issues here. Feature requests and questions should be posted at discussions.
+
+
- [ ] goldmark is fully compliant with the CommonMark. Before submitting issue, you **must** read [CommonMark spec](https://spec.commonmark.org/0.29/) and confirm your output is different from [CommonMark online demo](https://spec.commonmark.org/dingus/).
- [ ] **Extensions(Autolink without `<` `>`, Table, etc) are not part of CommonMark spec.** You should confirm your output is different from other official renderers correspond with an extension.
- [ ] **goldmark is not dedicated for Hugo**. If you are Hugo user and your issue was raised by your experience in Hugo, **you should consider create issue at Hugo repository at first** .
-- [ ] Before you make a feature request, **you should consider implement the new feature as an extension by yourself** . To keep goldmark itself simple, most new features should be implemented as an extension.
Please answer the following before submitting your issue:
@@ -12,6 +15,3 @@ Please answer the following before submitting your issue:
5. What did you expect to see? :
6. What did you see instead? :
7. Did you confirm your output is different from [CommonMark online demo](https://spec.commonmark.org/dingus/) or other official renderer correspond with an extension?:
-8. (Feature request only): Why you can not implement it as an extension?:
- - You should avoid saying like "I'm not familiar with this project" "I'm not a Go programmer" as far as possible. This is an open source project and a library for Go programmers. I encourage you to strive to read source codes.
- - I absolutely welcome questions that are difficult even if you read the source codes.
diff --git a/README.md b/README.md
index 0c11794..d6d57d8 100644
--- a/README.md
+++ b/README.md
@@ -287,6 +287,89 @@ markdown := goldmark.New(
)
```
+### Footnotes extension
+
+The Footnote extension implements [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes).
+
+This extension has some options:
+
+| Functional option | Type | Description |
+| ----------------- | ---- | ----------- |
+| `extension.WithFootnoteIDPrefix` | `[]byte` | a prefix for the id attributes.|
+| `extension.WithFootnoteIDPrefixFunction` | `func(gast.Node) []byte` | a function that determines the id attribute for given Node.|
+| `extension.WithFootnoteLinkTitle` | `[]byte` | an optional title attribute for footnote links.|
+| `extension.WithFootnoteBacklinkTitle` | `[]byte` | an optional title attribute for footnote backlinks. |
+| `extension.WithFootnoteLinkClass` | `[]byte` | a class for footnote links. This defaults to `footnote-ref`. |
+| `extension.WithFootnoteBacklinkClass` | `[]byte` | a class for footnote backlinks. This defaults to `footnote-backref`. |
+| `extension.WithFootnoteBacklinkHTML` | `[]byte` | a class for footnote backlinks. This defaults to `↩︎`. |
+
+Some options can have special substitutions. Occurances of “^^” in the string will be replaced by the corresponding footnote number in the HTML output. Occurances of “%%” will be replaced by a number for the reference (footnotes can have multiple references).
+
+`extension.WithFootnoteIDPrefix` and `extension.WithFootnoteIDPrefixFunction` are useful if you have multiple Markdown documents displayed inside one HTML document to avoid footnote ids to clash each other.
+
+`extension.WithFootnoteIDPrefix` sets fixed id prefix, so you may write codes like the following:
+
+```go
+for _, path := range files {
+ source := readAll(path)
+ prefix := getPrefix(path)
+
+ markdown := goldmark.New(
+ goldmark.WithExtensions(
+ NewFootnote(
+ WithFootnoteIDPrefix([]byte(path)),
+ ),
+ ),
+ )
+ var b bytes.Buffer
+ err := markdown.Convert(source, &b)
+ if err != nil {
+ t.Error(err.Error())
+ }
+}
+```
+
+`extension.WithFootnoteIDPrefixFunction` determines an id prefix by calling given function, so you may write codes like the following:
+
+```go
+markdown := goldmark.New(
+ goldmark.WithExtensions(
+ NewFootnote(
+ WithFootnoteIDPrefixFunction(func(n gast.Node) []byte {
+ v, ok := n.OwnerDocument().Meta()["footnote-prefix"]
+ if ok {
+ return util.StringToReadOnlyBytes(v.(string))
+ }
+ return nil
+ }),
+ ),
+ ),
+)
+
+for _, path := range files {
+ source := readAll(path)
+ var b bytes.Buffer
+
+ doc := markdown.Parser().Parse(text.NewReader(source))
+ doc.Meta()["footnote-prefix"] = getPrefix(path)
+ err := markdown.Renderer().Render(&b, source, doc)
+}
+```
+
+You can use [goldmark-meta](https://github.com/yuin/goldmark-meta) to define a id prefix in the markdown document:
+
+
+```markdown
+---
+title: document title
+slug: article1
+footnote-prefix: article1
+---
+
+# My article
+
+```
+
Security
--------------------
By default, goldmark does not render raw HTML or potentially-dangerous URLs.
diff --git a/ast/ast.go b/ast/ast.go
index 66059e9..9bffe67 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -116,6 +116,11 @@ type Node interface {
// tail of the children.
InsertAfter(self, v1, insertee Node)
+ // OwnerDocument returns this node's owner document.
+ // If this node is not a child of the Document node, OwnerDocument
+ // returns nil.
+ OwnerDocument() *Document
+
// Dump dumps an AST tree structure to stdout.
// This function completely aimed for debugging.
// level is a indent level. Implementer should indent informations with
@@ -358,6 +363,22 @@ func (n *BaseNode) InsertBefore(self, v1, insertee Node) {
}
}
+// OwnerDocument implements Node.OwnerDocument
+func (n *BaseNode) OwnerDocument() *Document {
+ d := n.Parent()
+ for {
+ p := d.Parent()
+ if p == nil {
+ if v, ok := d.(*Document); ok {
+ return v
+ }
+ break
+ }
+ d = p
+ }
+ return nil
+}
+
// Text implements Node.Text .
func (n *BaseNode) Text(source []byte) []byte {
var buf bytes.Buffer
diff --git a/ast/block.go b/ast/block.go
index f5bca33..d473482 100644
--- a/ast/block.go
+++ b/ast/block.go
@@ -50,6 +50,8 @@ func (b *BaseBlock) SetLines(v *textm.Segments) {
// A Document struct is a root node of Markdown text.
type Document struct {
BaseBlock
+
+ meta map[string]interface{}
}
// KindDocument is a NodeKind of the Document node.
@@ -70,10 +72,29 @@ func (n *Document) Kind() NodeKind {
return KindDocument
}
+// OwnerDocument implements Node.OwnerDocument
+func (n *Document) OwnerDocument() *Document {
+ return n
+}
+
+// Meta returns metadata of this document.
+func (n *Document) Meta() map[string]interface{} {
+ if n.meta == nil {
+ n.meta = map[string]interface{}{}
+ }
+ return n.meta
+}
+
+// SetMeta sets given metadata to this document.
+func (n *Document) SetMeta(meta map[string]interface{}) {
+ n.meta = meta
+}
+
// NewDocument returns a new Document node.
func NewDocument() *Document {
return &Document{
BaseBlock: BaseBlock{},
+ meta: nil,
}
}
diff --git a/extension/ast/footnote.go b/extension/ast/footnote.go
index 835f847..dedbab4 100644
--- a/extension/ast/footnote.go
+++ b/extension/ast/footnote.go
@@ -2,6 +2,7 @@ package ast
import (
"fmt"
+
gast "github.com/yuin/goldmark/ast"
)
@@ -9,13 +10,15 @@ import (
// (PHP Markdown Extra) text.
type FootnoteLink struct {
gast.BaseInline
- Index int
+ Index int
+ RefCount int
}
// Dump implements Node.Dump.
func (n *FootnoteLink) Dump(source []byte, level int) {
m := map[string]string{}
m["Index"] = fmt.Sprintf("%v", n.Index)
+ m["RefCount"] = fmt.Sprintf("%v", n.RefCount)
gast.DumpHelper(n, source, level, m, nil)
}
@@ -30,36 +33,40 @@ func (n *FootnoteLink) Kind() gast.NodeKind {
// NewFootnoteLink returns a new FootnoteLink node.
func NewFootnoteLink(index int) *FootnoteLink {
return &FootnoteLink{
- Index: index,
+ Index: index,
+ RefCount: 0,
}
}
-// A FootnoteBackLink struct represents a link to a footnote of Markdown
+// A FootnoteBacklink struct represents a link to a footnote of Markdown
// (PHP Markdown Extra) text.
-type FootnoteBackLink struct {
+type FootnoteBacklink struct {
gast.BaseInline
- Index int
+ Index int
+ RefCount int
}
// Dump implements Node.Dump.
-func (n *FootnoteBackLink) Dump(source []byte, level int) {
+func (n *FootnoteBacklink) Dump(source []byte, level int) {
m := map[string]string{}
m["Index"] = fmt.Sprintf("%v", n.Index)
+ m["RefCount"] = fmt.Sprintf("%v", n.RefCount)
gast.DumpHelper(n, source, level, m, nil)
}
-// KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node.
-var KindFootnoteBackLink = gast.NewNodeKind("FootnoteBackLink")
+// KindFootnoteBacklink is a NodeKind of the FootnoteBacklink node.
+var KindFootnoteBacklink = gast.NewNodeKind("FootnoteBacklink")
// Kind implements Node.Kind.
-func (n *FootnoteBackLink) Kind() gast.NodeKind {
- return KindFootnoteBackLink
+func (n *FootnoteBacklink) Kind() gast.NodeKind {
+ return KindFootnoteBacklink
}
-// NewFootnoteBackLink returns a new FootnoteBackLink node.
-func NewFootnoteBackLink(index int) *FootnoteBackLink {
- return &FootnoteBackLink{
- Index: index,
+// NewFootnoteBacklink returns a new FootnoteBacklink node.
+func NewFootnoteBacklink(index int) *FootnoteBacklink {
+ return &FootnoteBacklink{
+ Index: index,
+ RefCount: 0,
}
}
diff --git a/extension/footnote.go b/extension/footnote.go
index ede72db..0f7387a 100644
--- a/extension/footnote.go
+++ b/extension/footnote.go
@@ -2,6 +2,8 @@ package extension
import (
"bytes"
+ "strconv"
+
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
@@ -10,10 +12,10 @@ import (
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
- "strconv"
)
var footnoteListKey = parser.NewContextKey()
+var footnoteLinkListKey = parser.NewContextKey()
type footnoteBlockParser struct {
}
@@ -164,7 +166,17 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co
return nil
}
- return ast.NewFootnoteLink(index)
+ 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))
+
+ return fnlink
}
type footnoteASTTransformer struct {
@@ -180,23 +192,46 @@ func NewFootnoteASTTransformer() parser.ASTTransformer {
func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
var list *ast.FootnoteList
- if tlist := pc.Get(footnoteListKey); tlist != nil {
- list = tlist.(*ast.FootnoteList)
- } else {
- return
+ 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]++
+ }
+ }
+ for _, fnlink := range fnlist {
+ fnlink.RefCount = counter[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
}
- index := footnote.(*ast.Footnote).Index
+ fn := footnote.(*ast.Footnote)
+ index := fn.Index
if index < 0 {
list.RemoveChild(list, footnote)
} else {
- container.AppendChild(container, ast.NewFootnoteBackLink(index))
+ backLink := ast.NewFootnoteBacklink(index)
+ backLink.RefCount = counter[index]
+ container.AppendChild(container, backLink)
}
footnote = next
}
@@ -214,19 +249,250 @@ func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Read
node.AppendChild(node, list)
}
+// FootnoteConfig holds configuration values for the footnote extension.
+//
+// Link* and Backlink* configurations have some variables:
+// Occurances of “^^” in the string will be replaced by the
+// corresponding footnote number in the HTML output.
+// Occurances 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 {
- html.Config
+ FootnoteConfig
}
// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
-func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
+func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer {
r := &FootnoteHTMLRenderer{
- Config: html.NewConfig(),
+ FootnoteConfig: NewFootnoteConfig(),
}
for _, opt := range opts {
- opt.SetHTMLOption(&r.Config)
+ opt.SetFootnoteOption(&r.FootnoteConfig)
}
return r
}
@@ -234,7 +500,7 @@ func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
// 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.KindFootnoteBacklink, r.renderFootnoteBacklink)
reg.Register(ast.KindFootnote, r.renderFootnote)
reg.Register(ast.KindFootnoteList, r.renderFootnoteList)
}
@@ -243,25 +509,45 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
if entering {
n := node.(*ast.FootnoteLink)
is := strconv.Itoa(n.Index)
- _, _ = w.WriteString(``)
}
return gast.WalkContinue, nil
}
-func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
- n := node.(*ast.FootnoteBackLink)
+ n := node.(*ast.FootnoteBacklink)
is := strconv.Itoa(n.Index)
- _, _ = w.WriteString(` `)
}
return gast.WalkContinue, nil
@@ -271,7 +557,9 @@ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, n
n := node.(*ast.Footnote)
is := strconv.Itoa(n.Index)
if entering {
- _, _ = w.WriteString(`That's some text with a footnote.1
+Same footnote.1
+Another one.2
+
+`,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithParserOptions(
+ parser.WithASTTransformers(
+ util.Prioritized(&footnoteID{}, 100),
+ ),
+ ),
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewFootnote(
+ WithFootnoteIDPrefixFunction(func(n gast.Node) []byte {
+ v, ok := n.OwnerDocument().Meta()["footnote-prefix"]
+ if ok {
+ return util.StringToReadOnlyBytes(v.(string))
+ }
+ return nil
+ }),
+ WithFootnoteLinkClass([]byte("link-class")),
+ WithFootnoteBacklinkClass([]byte("backlink-class")),
+ WithFootnoteLinkTitle([]byte("link-title-%%-^^")),
+ WithFootnoteBacklinkTitle([]byte("backlink-title")),
+ WithFootnoteBacklinkHTML([]byte("^")),
+ ),
+ ),
+ )
+
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Footnote with an id prefix function",
+ Markdown: `That's some text with a footnote.[^1]
+
+Same footnote.[^1]
+
+Another one.[^2]
+
+[^1]: And that's the footnote.
+[^2]: Another footnote.
+`,
+ Expected: `That's some text with a footnote.1
+Same footnote.1
+Another one.2
+
+`,
+ },
+ t,
+ )
+}
diff --git a/testutil/testutil.go b/testutil/testutil.go
index e24f89d..3e575b1 100644
--- a/testutil/testutil.go
+++ b/testutil/testutil.go
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/util"
)
@@ -127,14 +128,14 @@ func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int)
}
// DoTestCases runs a set of test cases.
-func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT) {
+func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
for _, testCase := range cases {
- DoTestCase(m, testCase, t)
+ DoTestCase(m, testCase, t, opts...)
}
}
// DoTestCase runs a test case.
-func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT) {
+func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
var ok bool
var out bytes.Buffer
defer func() {
@@ -176,7 +177,7 @@ Actual
}
}()
- if err := m.Convert([]byte(testCase.Markdown), &out); err != nil {
+ if err := m.Convert([]byte(testCase.Markdown), &out, opts...); err != nil {
panic(err)
}
ok = bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(testCase.Expected)))