251 lines
7.4 KiB
JavaScript
251 lines
7.4 KiB
JavaScript
|
/**
|
||
|
* @typedef {import('hast').Element} Element
|
||
|
* @typedef {import('hast').ElementContent} ElementContent
|
||
|
*
|
||
|
* @typedef {import('./state.js').State} State
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @callback FootnoteBackContentTemplate
|
||
|
* Generate content for the backreference dynamically.
|
||
|
*
|
||
|
* For the following markdown:
|
||
|
*
|
||
|
* ```markdown
|
||
|
* Alpha[^micromark], bravo[^micromark], and charlie[^remark].
|
||
|
*
|
||
|
* [^remark]: things about remark
|
||
|
* [^micromark]: things about micromark
|
||
|
* ```
|
||
|
*
|
||
|
* This function will be called with:
|
||
|
*
|
||
|
* * `0` and `0` for the backreference from `things about micromark` to
|
||
|
* `alpha`, as it is the first used definition, and the first call to it
|
||
|
* * `0` and `1` for the backreference from `things about micromark` to
|
||
|
* `bravo`, as it is the first used definition, and the second call to it
|
||
|
* * `1` and `0` for the backreference from `things about remark` to
|
||
|
* `charlie`, as it is the second used definition
|
||
|
* @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 {Array<ElementContent> | ElementContent | string}
|
||
|
* Content for the backreference when linking back from definitions to their
|
||
|
* reference.
|
||
|
*
|
||
|
* @callback FootnoteBackLabelTemplate
|
||
|
* Generate a back label dynamically.
|
||
|
*
|
||
|
* For the following markdown:
|
||
|
*
|
||
|
* ```markdown
|
||
|
* Alpha[^micromark], bravo[^micromark], and charlie[^remark].
|
||
|
*
|
||
|
* [^remark]: things about remark
|
||
|
* [^micromark]: things about micromark
|
||
|
* ```
|
||
|
*
|
||
|
* This function will be called with:
|
||
|
*
|
||
|
* * `0` and `0` for the backreference from `things about micromark` to
|
||
|
* `alpha`, as it is the first used definition, and the first call to it
|
||
|
* * `0` and `1` for the backreference from `things about micromark` to
|
||
|
* `bravo`, as it is the first used definition, and the second call to it
|
||
|
* * `1` and `0` for the backreference from `things about remark` to
|
||
|
* `charlie`, as it is the second used definition
|
||
|
* @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}
|
||
|
* Back label to use when linking back from definitions to their reference.
|
||
|
*/
|
||
|
|
||
|
import structuredClone from '@ungap/structured-clone'
|
||
|
import {normalizeUri} from 'micromark-util-sanitize-uri'
|
||
|
|
||
|
/**
|
||
|
* Generate the default content that GitHub uses on backreferences.
|
||
|
*
|
||
|
* @param {number} _
|
||
|
* 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 {Array<ElementContent>}
|
||
|
* Content.
|
||
|
*/
|
||
|
export function defaultFootnoteBackContent(_, rereferenceIndex) {
|
||
|
/** @type {Array<ElementContent>} */
|
||
|
const result = [{type: 'text', value: '↩'}]
|
||
|
|
||
|
if (rereferenceIndex > 1) {
|
||
|
result.push({
|
||
|
type: 'element',
|
||
|
tagName: 'sup',
|
||
|
properties: {},
|
||
|
children: [{type: 'text', value: String(rereferenceIndex)}]
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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}
|
||
|
* Label.
|
||
|
*/
|
||
|
export function defaultFootnoteBackLabel(referenceIndex, rereferenceIndex) {
|
||
|
return (
|
||
|
'Back to reference ' +
|
||
|
(referenceIndex + 1) +
|
||
|
(rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate a hast footer for called footnote definitions.
|
||
|
*
|
||
|
* @param {State} state
|
||
|
* Info passed around.
|
||
|
* @returns {Element | undefined}
|
||
|
* `section` element or `undefined`.
|
||
|
*/
|
||
|
// eslint-disable-next-line complexity
|
||
|
export function footer(state) {
|
||
|
const clobberPrefix =
|
||
|
typeof state.options.clobberPrefix === 'string'
|
||
|
? state.options.clobberPrefix
|
||
|
: 'user-content-'
|
||
|
const footnoteBackContent =
|
||
|
state.options.footnoteBackContent || defaultFootnoteBackContent
|
||
|
const footnoteBackLabel =
|
||
|
state.options.footnoteBackLabel || defaultFootnoteBackLabel
|
||
|
const footnoteLabel = state.options.footnoteLabel || 'Footnotes'
|
||
|
const footnoteLabelTagName = state.options.footnoteLabelTagName || 'h2'
|
||
|
const footnoteLabelProperties = state.options.footnoteLabelProperties || {
|
||
|
className: ['sr-only']
|
||
|
}
|
||
|
/** @type {Array<ElementContent>} */
|
||
|
const listItems = []
|
||
|
let referenceIndex = -1
|
||
|
|
||
|
while (++referenceIndex < state.footnoteOrder.length) {
|
||
|
const def = state.footnoteById.get(state.footnoteOrder[referenceIndex])
|
||
|
|
||
|
if (!def) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
const content = state.all(def)
|
||
|
const id = String(def.identifier).toUpperCase()
|
||
|
const safeId = normalizeUri(id.toLowerCase())
|
||
|
let rereferenceIndex = 0
|
||
|
/** @type {Array<ElementContent>} */
|
||
|
const backReferences = []
|
||
|
const counts = state.footnoteCounts.get(id)
|
||
|
|
||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||
|
while (counts !== undefined && ++rereferenceIndex <= counts) {
|
||
|
if (backReferences.length > 0) {
|
||
|
backReferences.push({type: 'text', value: ' '})
|
||
|
}
|
||
|
|
||
|
let children =
|
||
|
typeof footnoteBackContent === 'string'
|
||
|
? footnoteBackContent
|
||
|
: footnoteBackContent(referenceIndex, rereferenceIndex)
|
||
|
|
||
|
if (typeof children === 'string') {
|
||
|
children = {type: 'text', value: children}
|
||
|
}
|
||
|
|
||
|
backReferences.push({
|
||
|
type: 'element',
|
||
|
tagName: 'a',
|
||
|
properties: {
|
||
|
href:
|
||
|
'#' +
|
||
|
clobberPrefix +
|
||
|
'fnref-' +
|
||
|
safeId +
|
||
|
(rereferenceIndex > 1 ? '-' + rereferenceIndex : ''),
|
||
|
dataFootnoteBackref: '',
|
||
|
ariaLabel:
|
||
|
typeof footnoteBackLabel === 'string'
|
||
|
? footnoteBackLabel
|
||
|
: footnoteBackLabel(referenceIndex, rereferenceIndex),
|
||
|
className: ['data-footnote-backref']
|
||
|
},
|
||
|
children: Array.isArray(children) ? children : [children]
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const tail = content[content.length - 1]
|
||
|
|
||
|
if (tail && tail.type === 'element' && tail.tagName === 'p') {
|
||
|
const tailTail = tail.children[tail.children.length - 1]
|
||
|
if (tailTail && tailTail.type === 'text') {
|
||
|
tailTail.value += ' '
|
||
|
} else {
|
||
|
tail.children.push({type: 'text', value: ' '})
|
||
|
}
|
||
|
|
||
|
tail.children.push(...backReferences)
|
||
|
} else {
|
||
|
content.push(...backReferences)
|
||
|
}
|
||
|
|
||
|
/** @type {Element} */
|
||
|
const listItem = {
|
||
|
type: 'element',
|
||
|
tagName: 'li',
|
||
|
properties: {id: clobberPrefix + 'fn-' + safeId},
|
||
|
children: state.wrap(content, true)
|
||
|
}
|
||
|
|
||
|
state.patch(def, listItem)
|
||
|
|
||
|
listItems.push(listItem)
|
||
|
}
|
||
|
|
||
|
if (listItems.length === 0) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
type: 'element',
|
||
|
tagName: 'section',
|
||
|
properties: {dataFootnotes: true, className: ['footnotes']},
|
||
|
children: [
|
||
|
{
|
||
|
type: 'element',
|
||
|
tagName: footnoteLabelTagName,
|
||
|
properties: {
|
||
|
...structuredClone(footnoteLabelProperties),
|
||
|
id: 'footnote-label'
|
||
|
},
|
||
|
children: [{type: 'text', value: footnoteLabel}]
|
||
|
},
|
||
|
{type: 'text', value: '\n'},
|
||
|
{
|
||
|
type: 'element',
|
||
|
tagName: 'ol',
|
||
|
properties: {},
|
||
|
children: state.wrap(listItems, true)
|
||
|
},
|
||
|
{type: 'text', value: '\n'}
|
||
|
]
|
||
|
}
|
||
|
}
|