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 {}
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
var closes int
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 {}
func NewFootnoteParser () parser .InlineParser {
return defaultFootnoteParser
}
func (s *footnoteParser ) Trigger () []byte {
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 ++
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 {}
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 )
}
type FootnoteConfig struct {
html .Config
IDPrefix []byte
IDPrefixFunction func (gast .Node ) []byte
LinkTitle []byte
BacklinkTitle []byte
LinkClass []byte
BacklinkClass []byte
BacklinkHTML []byte
}
type FootnoteOption interface {
renderer .Option
SetFootnoteOption (*FootnoteConfig )
}
func NewFootnoteConfig () FootnoteConfig {
return FootnoteConfig {
Config : html .NewConfig (),
LinkTitle : []byte ("" ),
BacklinkTitle : []byte ("" ),
LinkClass : []byte ("footnote-ref" ),
BacklinkClass : []byte ("footnote-backref" ),
BacklinkHTML : []byte ("↩︎" ),
}
}
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 )
}
}
}
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
}
func WithFootnoteIDPrefix [T []byte | string ](a T ) FootnoteOption {
return &withFootnoteIDPrefix {[]byte (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
}
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
}
func WithFootnoteLinkTitle [T []byte | string ](a T ) FootnoteOption {
return &withFootnoteLinkTitle {[]byte (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
}
func WithFootnoteBacklinkTitle [T []byte | string ](a T ) FootnoteOption {
return &withFootnoteBacklinkTitle {[]byte (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
}
func WithFootnoteLinkClass [T []byte | string ](a T ) FootnoteOption {
return &withFootnoteLinkClass {[]byte (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
}
func WithFootnoteBacklinkClass [T []byte | string ](a T ) FootnoteOption {
return &withFootnoteBacklinkClass {[]byte (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
}
func WithFootnoteBacklinkHTML [T []byte | string ](a T ) FootnoteOption {
return &withFootnoteBacklinkHTML {[]byte (a )}
}
type FootnoteHTMLRenderer struct {
FootnoteConfig
}
func NewFootnoteHTMLRenderer (opts ...FootnoteOption ) renderer .NodeRenderer {
r := &FootnoteHTMLRenderer {
FootnoteConfig : NewFootnoteConfig (),
}
for _ , opt := range opts {
opt .SetFootnoteOption (&r .FootnoteConfig )
}
return r
}
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 (`<sup id="` )
_, _ = w .Write (r .idPrefix (node ))
_, _ = w .WriteString (`fnref` )
if n .RefIndex > 0 {
_, _ = w .WriteString (fmt .Sprintf ("%v" , n .RefIndex ))
}
_ = w .WriteByte (':' )
_, _ = w .WriteString (is )
_, _ = w .WriteString (`"><a href="#` )
_, _ = w .Write (r .idPrefix (node ))
_, _ = w .WriteString (`fn:` )
_, _ = w .WriteString (is )
_, _ = w .WriteString (`" class="` )
_, _ = w .Write (applyFootnoteTemplate (r .FootnoteConfig .LinkClass ,
n .Index , n .RefCount ))
if len (r .FootnoteConfig .LinkTitle ) > 0 {
_, _ = w .WriteString (`" title="` )
_, _ = w .Write (util .EscapeHTML (applyFootnoteTemplate (r .FootnoteConfig .LinkTitle , n .Index , n .RefCount )))
}
_, _ = w .WriteString (`" role="doc-noteref">` )
_, _ = w .WriteString (is )
_, _ = w .WriteString (`</a></sup>` )
}
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 (` <a href="#` )
_, _ = w .Write (r .idPrefix (node ))
_, _ = w .WriteString (`fnref` )
if n .RefIndex > 0 {
_, _ = w .WriteString (fmt .Sprintf ("%v" , n .RefIndex ))
}
_ = w .WriteByte (':' )
_, _ = w .WriteString (is )
_, _ = w .WriteString (`" class="` )
_, _ = w .Write (applyFootnoteTemplate (r .FootnoteConfig .BacklinkClass , n .Index , n .RefCount ))
if len (r .FootnoteConfig .BacklinkTitle ) > 0 {
_, _ = w .WriteString (`" title="` )
_, _ = w .Write (util .EscapeHTML (applyFootnoteTemplate (r .FootnoteConfig .BacklinkTitle , n .Index , n .RefCount )))
}
_, _ = w .WriteString (`" role="doc-backlink">` )
_, _ = w .Write (applyFootnoteTemplate (r .FootnoteConfig .BacklinkHTML , n .Index , n .RefCount ))
_, _ = w .WriteString (`</a>` )
}
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 (`<li id="` )
_, _ = w .Write (r .idPrefix (node ))
_, _ = w .WriteString (`fn:` )
_, _ = w .WriteString (is )
_, _ = w .WriteString (`"` )
if node .Attributes () != nil {
html .RenderAttributes (w , node , html .ListItemAttributeFilter )
}
_, _ = w .WriteString (">\n" )
} else {
_, _ = w .WriteString ("</li>\n" )
}
return gast .WalkContinue , nil
}
func (r *FootnoteHTMLRenderer ) renderFootnoteList (
w util .BufWriter , source []byte , node gast .Node , entering bool ) (gast .WalkStatus , error ) {
if entering {
_, _ = w .WriteString (`<div class="footnotes" role="doc-endnotes"` )
if node .Attributes () != nil {
html .RenderAttributes (w , node , html .GlobalAttributeFilter )
}
_ = w .WriteByte ('>' )
if r .Config .XHTML {
_, _ = w .WriteString ("\n<hr />\n" )
} else {
_, _ = w .WriteString ("\n<hr>\n" )
}
_, _ = w .WriteString ("<ol>\n" )
} else {
_, _ = w .WriteString ("</ol>\n" )
_, _ = w .WriteString ("</div>\n" )
}
return gast .WalkContinue , nil
}
func (r *FootnoteHTMLRenderer ) idPrefix (node gast .Node ) []byte {
if r .FootnoteConfig .IDPrefix != nil {
return r .FootnoteConfig .IDPrefix
}
if r .FootnoteConfig .IDPrefixFunction != nil {
return r .FootnoteConfig .IDPrefixFunction (node )
}
return []byte ("" )
}
func applyFootnoteTemplate(b []byte , index , refCount int ) []byte {
fast := true
for i , c := range b {
if i != 0 {
if b [i -1 ] == '^' && c == '^' {
fast = false
break
}
if b [i -1 ] == '%' && c == '%' {
fast = false
break
}
}
}
if fast {
return b
}
is := []byte (strconv .Itoa (index ))
rs := []byte (strconv .Itoa (refCount ))
ret := bytes .Replace (b , []byte ("^^" ), is , -1 )
return bytes .Replace (ret , []byte ("%%" ), rs , -1 )
}
type footnote struct {
options []FootnoteOption
}
var Footnote = &footnote {
options : []FootnoteOption {},
}
func NewFootnote (opts ...FootnoteOption ) goldmark .Extender {
return &footnote {
options : opts ,
}
}
func (e *footnote ) Extend (m goldmark .Markdown ) {
m .Parser ().AddOptions (
parser .WithBlockParsers (
util .Prioritized (NewFootnoteBlockParser (), 999 ),
),
parser .WithInlineParsers (
util .Prioritized (NewFootnoteParser (), 101 ),
),
parser .WithASTTransformers (
util .Prioritized (NewFootnoteASTTransformer (), 999 ),
),
)
m .Renderer ().AddOptions (renderer .WithNodeRenderers (
util .Prioritized (NewFootnoteHTMLRenderer (e .options ...), 500 ),
))
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .