/** * @import { * CompileContext, * Extension as FromMarkdownExtension, * Handle as FromMarkdownHandle * } from 'mdast-util-from-markdown' * @import {ToMarkdownOptions} from 'mdast-util-gfm-footnote' * @import { * Handle as ToMarkdownHandle, * Map, * Options as ToMarkdownExtension * } from 'mdast-util-to-markdown' * @import {FootnoteDefinition, FootnoteReference} from 'mdast' */ import {ok as assert} from 'devlop' import {normalizeIdentifier} from 'micromark-util-normalize-identifier' footnoteReference.peek = footnoteReferencePeek /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function enterFootnoteCallString() { this.buffer() } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function enterFootnoteCall(token) { this.enter({type: 'footnoteReference', identifier: '', label: ''}, token) } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function enterFootnoteDefinitionLabelString() { this.buffer() } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function enterFootnoteDefinition(token) { this.enter( {type: 'footnoteDefinition', identifier: '', label: '', children: []}, token ) } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function exitFootnoteCallString(token) { const label = this.resume() const node = this.stack[this.stack.length - 1] assert(node.type === 'footnoteReference') node.identifier = normalizeIdentifier( this.sliceSerialize(token) ).toLowerCase() node.label = label } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function exitFootnoteCall(token) { this.exit(token) } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function exitFootnoteDefinitionLabelString(token) { const label = this.resume() const node = this.stack[this.stack.length - 1] assert(node.type === 'footnoteDefinition') node.identifier = normalizeIdentifier( this.sliceSerialize(token) ).toLowerCase() node.label = label } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function exitFootnoteDefinition(token) { this.exit(token) } /** @type {ToMarkdownHandle} */ function footnoteReferencePeek() { return '[' } /** * @type {ToMarkdownHandle} * @param {FootnoteReference} node */ function footnoteReference(node, _, state, info) { const tracker = state.createTracker(info) let value = tracker.move('[^') const exit = state.enter('footnoteReference') const subexit = state.enter('reference') value += tracker.move( state.safe(state.associationId(node), {after: ']', before: value}) ) subexit() exit() value += tracker.move(']') return value } /** * Create an extension for `mdast-util-from-markdown` to enable GFM footnotes * in markdown. * * @returns {FromMarkdownExtension} * Extension for `mdast-util-from-markdown`. */ export function gfmFootnoteFromMarkdown() { return { enter: { gfmFootnoteCallString: enterFootnoteCallString, gfmFootnoteCall: enterFootnoteCall, gfmFootnoteDefinitionLabelString: enterFootnoteDefinitionLabelString, gfmFootnoteDefinition: enterFootnoteDefinition }, exit: { gfmFootnoteCallString: exitFootnoteCallString, gfmFootnoteCall: exitFootnoteCall, gfmFootnoteDefinitionLabelString: exitFootnoteDefinitionLabelString, gfmFootnoteDefinition: exitFootnoteDefinition } } } /** * Create an extension for `mdast-util-to-markdown` to enable GFM footnotes * in markdown. * * @param {ToMarkdownOptions | null | undefined} [options] * Configuration (optional). * @returns {ToMarkdownExtension} * Extension for `mdast-util-to-markdown`. */ export function gfmFootnoteToMarkdown(options) { // To do: next major: change default. let firstLineBlank = false if (options && options.firstLineBlank) { firstLineBlank = true } return { handlers: {footnoteDefinition, footnoteReference}, // This is on by default already. unsafe: [{character: '[', inConstruct: ['label', 'phrasing', 'reference']}] } /** * @type {ToMarkdownHandle} * @param {FootnoteDefinition} node */ function footnoteDefinition(node, _, state, info) { const tracker = state.createTracker(info) let value = tracker.move('[^') const exit = state.enter('footnoteDefinition') const subexit = state.enter('label') value += tracker.move( state.safe(state.associationId(node), {before: value, after: ']'}) ) subexit() value += tracker.move(']:') if (node.children && node.children.length > 0) { tracker.shift(4) value += tracker.move( (firstLineBlank ? '\n' : ' ') + state.indentLines( state.containerFlow(node, tracker.current()), firstLineBlank ? mapAll : mapExceptFirst ) ) } exit() return value } } /** @type {Map} */ function mapExceptFirst(line, index, blank) { return index === 0 ? line : mapAll(line, index, blank) } /** @type {Map} */ function mapAll(line, index, blank) { return (blank ? '' : ' ') + line }