/** * @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 | 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} * Content. */ export function defaultFootnoteBackContent(_, rereferenceIndex) { /** @type {Array} */ 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} */ 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} */ 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'} ] } }