feat: Support named footnote reference links

Uses the provided name for a PHP Markdown Extra Footnote reference when it is
not an incorrectly ordered number value, such as: [^named]. The reference names
were previously ignored and replaced with ordered index integers. According to:
https://michelf.ca/projects/php-markdown/extra/#footnotes

> Names can contain any character valid within an id attribute in HTML.
This commit is contained in:
Brad Erickson 2020-12-24 09:27:54 -08:00
parent 6c741ae251
commit 743f08bde8
3 changed files with 67 additions and 23 deletions

View file

@ -66,3 +66,26 @@ test![^1]
</ol> </ol>
</section> </section>
//= = = = = = = = = = = = = = = = = = = = = = = =// //= = = = = = = = = = = = = = = = = = = = = = = =//
6
//- - - - - - - - -//
test named[^named].
test incorrect index number[^42]
[^named]: footnote
[^42]: footnote
//- - - - - - - - -//
<p>test named<sup id="fnref:named"><a href="#fn:named" class="footnote-ref" role="doc-noteref">named</a></sup>.</p>
<p>test incorrect index number<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:named" role="doc-endnote">
<p>footnote <a href="#fnref:named" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>footnote <a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</section>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -12,6 +12,7 @@ type FootnoteLink struct {
gast.BaseInline gast.BaseInline
Index int Index int
RefCount int RefCount int
Ref []byte
} }
// Dump implements Node.Dump. // Dump implements Node.Dump.
@ -31,10 +32,11 @@ func (n *FootnoteLink) Kind() gast.NodeKind {
} }
// NewFootnoteLink returns a new FootnoteLink node. // NewFootnoteLink returns a new FootnoteLink node.
func NewFootnoteLink(index int) *FootnoteLink { func NewFootnoteLink(index int, ref []byte) *FootnoteLink {
return &FootnoteLink{ return &FootnoteLink{
Index: index, Index: index,
RefCount: 0, RefCount: 0,
Ref: ref,
} }
} }
@ -44,6 +46,7 @@ type FootnoteBacklink struct {
gast.BaseInline gast.BaseInline
Index int Index int
RefCount int RefCount int
Ref []byte
} }
// Dump implements Node.Dump. // Dump implements Node.Dump.
@ -63,10 +66,11 @@ func (n *FootnoteBacklink) Kind() gast.NodeKind {
} }
// NewFootnoteBacklink returns a new FootnoteBacklink node. // NewFootnoteBacklink returns a new FootnoteBacklink node.
func NewFootnoteBacklink(index int) *FootnoteBacklink { func NewFootnoteBacklink(index int, ref []byte) *FootnoteBacklink {
return &FootnoteBacklink{ return &FootnoteBacklink{
Index: index, Index: index,
RefCount: 0, RefCount: 0,
Ref: ref,
} }
} }

View file

@ -140,7 +140,7 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co
return nil return nil
} }
closes := pos + closure closes := pos + closure
value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes)) ref := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes))
block.Advance(closes + 1) block.Advance(closes + 1)
var list *ast.FootnoteList var list *ast.FootnoteList
@ -153,9 +153,9 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co
index := 0 index := 0
for def := list.FirstChild(); def != nil; def = def.NextSibling() { for def := list.FirstChild(); def != nil; def = def.NextSibling() {
d := def.(*ast.Footnote) d := def.(*ast.Footnote)
if bytes.Equal(d.Ref, value) { if bytes.Equal(d.Ref, ref) {
if d.Index < 0 { if d.Index < 0 {
list.Count += 1 list.Count++
d.Index = list.Count d.Index = list.Count
} }
index = d.Index index = d.Index
@ -166,7 +166,7 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co
return nil return nil
} }
fnlink := ast.NewFootnoteLink(index) fnlink := ast.NewFootnoteLink(index, ref)
var fnlist []*ast.FootnoteLink var fnlist []*ast.FootnoteLink
if tmp := pc.Get(footnoteLinkListKey); tmp != nil { if tmp := pc.Get(footnoteLinkListKey); tmp != nil {
fnlist = tmp.([]*ast.FootnoteLink) fnlist = tmp.([]*ast.FootnoteLink)
@ -232,7 +232,7 @@ func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Read
if index < 0 { if index < 0 {
list.RemoveChild(list, footnote) list.RemoveChild(list, footnote)
} else { } else {
backLink := ast.NewFootnoteBacklink(index) backLink := ast.NewFootnoteBacklink(index, fn.Ref)
backLink.RefCount = counter[index] backLink.RefCount = counter[index]
container.AppendChild(container, backLink) container.AppendChild(container, backLink)
} }
@ -511,25 +511,26 @@ func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegist
func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
if entering { if entering {
n := node.(*ast.FootnoteLink) n := node.(*ast.FootnoteLink)
is := strconv.Itoa(n.Index)
reference := r.getReference(n.Index, n.Ref)
_, _ = w.WriteString(`<sup id="`) _, _ = w.WriteString(`<sup id="`)
_, _ = w.Write(r.idPrefix(node)) _, _ = w.Write(r.idPrefix(node))
_, _ = w.WriteString(`fnref:`) _, _ = w.WriteString(`fnref:`)
_, _ = w.WriteString(is) _, _ = w.Write(reference)
_, _ = w.WriteString(`"><a href="#`) _, _ = w.WriteString(`"><a href="#`)
_, _ = w.Write(r.idPrefix(node)) _, _ = w.Write(r.idPrefix(node))
_, _ = w.WriteString(`fn:`) _, _ = w.WriteString(`fn:`)
_, _ = w.WriteString(is) _, _ = w.Write(reference)
_, _ = w.WriteString(`" class="`) _, _ = w.WriteString(`" class="`)
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.LinkClass, _, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.LinkClass,
n.Index, n.RefCount)) reference, n.RefCount))
if len(r.FootnoteConfig.LinkTitle) > 0 { if len(r.FootnoteConfig.LinkTitle) > 0 {
_, _ = w.WriteString(`" title="`) _, _ = w.WriteString(`" title="`)
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.LinkTitle, n.Index, n.RefCount))) _, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.LinkTitle, reference, n.RefCount)))
} }
_, _ = w.WriteString(`" role="doc-noteref">`) _, _ = w.WriteString(`" role="doc-noteref">`)
_, _ = w.WriteString(is) _, _ = w.Write(reference)
_, _ = w.WriteString(`</a></sup>`) _, _ = w.WriteString(`</a></sup>`)
} }
return gast.WalkContinue, nil return gast.WalkContinue, nil
@ -538,19 +539,20 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
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 { if entering {
n := node.(*ast.FootnoteBacklink) n := node.(*ast.FootnoteBacklink)
is := strconv.Itoa(n.Index)
reference := r.getReference(n.Index, n.Ref)
_, _ = w.WriteString(` <a href="#`) _, _ = w.WriteString(` <a href="#`)
_, _ = w.Write(r.idPrefix(node)) _, _ = w.Write(r.idPrefix(node))
_, _ = w.WriteString(`fnref:`) _, _ = w.WriteString(`fnref:`)
_, _ = w.WriteString(is) _, _ = w.Write(reference)
_, _ = w.WriteString(`" class="`) _, _ = w.WriteString(`" class="`)
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkClass, n.Index, n.RefCount)) _, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkClass, reference, n.RefCount))
if len(r.FootnoteConfig.BacklinkTitle) > 0 { if len(r.FootnoteConfig.BacklinkTitle) > 0 {
_, _ = w.WriteString(`" title="`) _, _ = w.WriteString(`" title="`)
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.BacklinkTitle, n.Index, n.RefCount))) _, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.BacklinkTitle, reference, n.RefCount)))
} }
_, _ = w.WriteString(`" role="doc-backlink">`) _, _ = w.WriteString(`" role="doc-backlink">`)
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, n.Index, n.RefCount)) _, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, reference, n.RefCount))
_, _ = w.WriteString(`</a>`) _, _ = w.WriteString(`</a>`)
} }
return gast.WalkContinue, nil return gast.WalkContinue, nil
@ -558,12 +560,13 @@ func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(w util.BufWriter, source [
func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
n := node.(*ast.Footnote) n := node.(*ast.Footnote)
is := strconv.Itoa(n.Index)
reference := r.getReference(n.Index, n.Ref)
if entering { if entering {
_, _ = w.WriteString(`<li id="`) _, _ = w.WriteString(`<li id="`)
_, _ = w.Write(r.idPrefix(node)) _, _ = w.Write(r.idPrefix(node))
_, _ = w.WriteString(`fn:`) _, _ = w.WriteString(`fn:`)
_, _ = w.WriteString(is) _, _ = w.Write(reference)
_, _ = w.WriteString(`" role="doc-endnote"`) _, _ = w.WriteString(`" role="doc-endnote"`)
if node.Attributes() != nil { if node.Attributes() != nil {
html.RenderAttributes(w, node, html.ListItemAttributeFilter) html.RenderAttributes(w, node, html.ListItemAttributeFilter)
@ -613,7 +616,21 @@ func (r *FootnoteHTMLRenderer) idPrefix(node gast.Node) []byte {
return []byte("") return []byte("")
} }
func applyFootnoteTemplate(b []byte, index, refCount int) []byte { // getReference returns the correct reference name value: named reference or
// sequential index number.
func (r *FootnoteHTMLRenderer) getReference(index int, ref []byte) []byte {
_, err := strconv.Atoi(string(ref))
isInt := err == nil
if isInt {
// ignore ref value and return the index when the ref is an int
return []byte(strconv.Itoa(index))
}
return ref
}
func applyFootnoteTemplate(b []byte, reference []byte, refCount int) []byte {
fast := true fast := true
for i, c := range b { for i, c := range b {
if i != 0 { if i != 0 {
@ -630,9 +647,9 @@ func applyFootnoteTemplate(b []byte, index, refCount int) []byte {
if fast { if fast {
return b return b
} }
is := []byte(strconv.Itoa(index))
rs := []byte(strconv.Itoa(refCount)) rs := []byte(strconv.Itoa(refCount))
ret := bytes.Replace(b, []byte("^^"), is, -1) ret := bytes.Replace(b, []byte("^^"), reference, -1)
return bytes.Replace(ret, []byte("%%"), rs, -1) return bytes.Replace(ret, []byte("%%"), rs, -1)
} }