/**
* @import {HtmlOptions as Options} from 'micromark-extension-gfm-footnote'
* @import {HtmlExtension} from 'micromark-util-types'
*/
import {ok as assert} from 'devlop'
import {normalizeIdentifier} from 'micromark-util-normalize-identifier'
import {sanitizeUri} from 'micromark-util-sanitize-uri'
const own = {}.hasOwnProperty
/** @type {Options} */
const emptyOptions = {}
/**
* Generate the default label that GitHub uses on backreferences.
*
* @param {number} referenceIndex
* Index of the definition in the order that they are first referenced,
* 0-indexed.
* @param {number} rereferenceIndex
* Index of calls to the same definition, 0-indexed.
* @returns {string}
* Default label.
*/
export function defaultBackLabel(referenceIndex, rereferenceIndex) {
return (
'Back to reference ' +
(referenceIndex + 1) +
(rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
)
}
/**
* Create an extension for `micromark` to support GFM footnotes when
* serializing to HTML.
*
* @param {Options | null | undefined} [options={}]
* Configuration (optional).
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions` to
* support GFM footnotes when serializing to HTML.
*/
export function gfmFootnoteHtml(options) {
const config = options || emptyOptions
const label = config.label || 'Footnotes'
const labelTagName = config.labelTagName || 'h2'
const labelAttributes =
config.labelAttributes === null || config.labelAttributes === undefined
? 'class="sr-only"'
: config.labelAttributes
const backLabel = config.backLabel || defaultBackLabel
const clobberPrefix =
config.clobberPrefix === null || config.clobberPrefix === undefined
? 'user-content-'
: config.clobberPrefix
return {
enter: {
gfmFootnoteDefinition() {
const stack = this.getData('tightStack')
stack.push(false)
},
gfmFootnoteDefinitionLabelString() {
this.buffer()
},
gfmFootnoteCallString() {
this.buffer()
}
},
exit: {
gfmFootnoteDefinition() {
let definitions = this.getData('gfmFootnoteDefinitions')
const footnoteStack = this.getData('gfmFootnoteDefinitionStack')
assert(footnoteStack, 'expected `footnoteStack`')
const tightStack = this.getData('tightStack')
const current = footnoteStack.pop()
const value = this.resume()
assert(current, 'expected to be in a footnote')
if (!definitions) {
this.setData('gfmFootnoteDefinitions', (definitions = {}))
}
if (!own.call(definitions, current)) definitions[current] = value
tightStack.pop()
this.setData('slurpOneLineEnding', true)
// “Hack” to prevent a line ending from showing up if we’re in a definition in
// an empty list item.
this.setData('lastWasTag')
},
gfmFootnoteDefinitionLabelString(token) {
let footnoteStack = this.getData('gfmFootnoteDefinitionStack')
if (!footnoteStack) {
this.setData('gfmFootnoteDefinitionStack', (footnoteStack = []))
}
footnoteStack.push(normalizeIdentifier(this.sliceSerialize(token)))
this.resume() // Drop the label.
this.buffer() // Get ready for a value.
},
gfmFootnoteCallString(token) {
let calls = this.getData('gfmFootnoteCallOrder')
let counts = this.getData('gfmFootnoteCallCounts')
const id = normalizeIdentifier(this.sliceSerialize(token))
/** @type {number} */
let counter
this.resume()
if (!calls) this.setData('gfmFootnoteCallOrder', (calls = []))
if (!counts) this.setData('gfmFootnoteCallCounts', (counts = {}))
const index = calls.indexOf(id)
const safeId = sanitizeUri(id.toLowerCase())
if (index === -1) {
calls.push(id)
counts[id] = 1
counter = calls.length
} else {
counts[id]++
counter = index + 1
}
const reuseCounter = counts[id]
this.tag(
'' +
String(counter) +
''
)
},
null() {
const calls = this.getData('gfmFootnoteCallOrder') || []
const counts = this.getData('gfmFootnoteCallCounts') || {}
const definitions = this.getData('gfmFootnoteDefinitions') || {}
let index = -1
if (calls.length > 0) {
this.lineEndingIfNeeded()
this.tag(
'')
}
}
}
}
}