@ -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,20 @@ 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 ) )
if line [ 0 ] == '!' {
parent . AppendChild ( parent , gast . NewTextSegment ( text . NewSegment ( segment . Start , segment . Start + 1 ) ) )
}
return fnlink
}
type footnoteASTTransformer struct {
@ -180,23 +195,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 +252,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:
// 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 {
html . Config
Footnote Config
}
// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
func NewFootnoteHTMLRenderer ( opts ... html . Option ) renderer . NodeRenderer {
func NewFootnoteHTMLRenderer ( opts ... Footnote Option) renderer . NodeRenderer {
r := & FootnoteHTMLRenderer {
Config : html . NewConfig ( ) ,
Footnote Config: NewFootnote Config ( ) ,
}
for _ , opt := range opts {
opt . SetHTMLOption ( & r . Config )
opt . SetFootnote Option ( & r . Footnote Config)
}
return r
}
@ -234,7 +503,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 . KindFootnoteBackL ink , r . renderFootnoteBackL ink )
reg . Register ( ast . KindFootnoteBackl ink , r . renderFootnoteBackl ink )
reg . Register ( ast . KindFootnote , r . renderFootnote )
reg . Register ( ast . KindFootnoteList , r . renderFootnoteList )
}
@ -243,25 +512,45 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
if entering {
n := node . ( * ast . FootnoteLink )
is := strconv . Itoa ( n . Index )
_ , _ = w . WriteString ( ` <sup id="fnref: ` )
_ , _ = w . WriteString ( ` <sup id=" ` )
_ , _ = w . Write ( r . idPrefix ( node ) )
_ , _ = w . WriteString ( ` fnref: ` )
_ , _ = w . WriteString ( is )
_ , _ = w . WriteString ( ` "><a href="#fn: ` )
_ , _ = w . WriteString ( ` "><a href="# ` )
_ , _ = w . Write ( r . idPrefix ( node ) )
_ , _ = w . WriteString ( ` fn: ` )
_ , _ = w . WriteString ( is )
_ , _ = w . WriteString ( ` " class="footnote-ref" role="doc-noteref"> ` )
_ , _ = 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 ) {
func ( r * FootnoteHTMLRenderer ) renderFootnoteBackl ink ( w util . BufWriter , source [ ] byte , node gast . Node , entering bool ) ( gast . WalkStatus , error ) {
if entering {
n := node . ( * ast . FootnoteBackL ink )
n := node . ( * ast . FootnoteBackl ink )
is := strconv . Itoa ( n . Index )
_ , _ = w . WriteString ( ` <a href="#fnref: ` )
_ , _ = w . WriteString ( ` <a href="# ` )
_ , _ = w . Write ( r . idPrefix ( node ) )
_ , _ = w . WriteString ( ` fnref: ` )
_ , _ = w . WriteString ( is )
_ , _ = w . WriteString ( ` " class="footnote-backref" role="doc-backlink"> ` )
_ , _ = w . WriteString ( "↩︎" )
_ , _ = 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
@ -271,7 +560,9 @@ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, n
n := node . ( * ast . Footnote )
is := strconv . Itoa ( n . Index )
if entering {
_ , _ = w . WriteString ( ` <li id="fn: ` )
_ , _ = w . WriteString ( ` <li id=" ` )
_ , _ = w . Write ( r . idPrefix ( node ) )
_ , _ = w . WriteString ( ` fn: ` )
_ , _ = w . WriteString ( is )
_ , _ = w . WriteString ( ` " role="doc-endnote" ` )
if node . Attributes ( ) != nil {
@ -312,11 +603,54 @@ func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byt
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
}
// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes.
var Footnote = & footnote { }
var Footnote = & footnote {
options : [ ] FootnoteOption { } ,
}
// NewFootnote returns a new extension with given options.
func NewFootnote ( opts ... FootnoteOption ) goldmark . Extender {
return & footnote {
options : opts ,
}
}
func ( e * footnote ) Extend ( m goldmark . Markdown ) {
m . Parser ( ) . AddOptions (
@ -331,6 +665,6 @@ func (e *footnote) Extend(m goldmark.Markdown) {
) ,
)
m . Renderer ( ) . AddOptions ( renderer . WithNodeRenderers (
util . Prioritized ( NewFootnoteHTMLRenderer ( ) , 500 ) ,
util . Prioritized ( NewFootnoteHTMLRenderer ( e . options ... ) , 500 ) ,
) )
}