import { fromMarkdown } from "mdast-util-from-markdown"; const INLINE_FOOTNOTE_PREFIX = "inline-footnote"; const findInlineFootnote = (value, fromIndex = 0) => { const start = value.indexOf("^[", fromIndex); if (start === -1) { return null; } let depth = 1; for (let index = start + 2; index < value.length; index += 1) { const character = value[index]; if (character === "\\" && index + 1 < value.length) { index += 1; continue; } if (character === "[") { depth += 1; continue; } if (character === "]") { depth -= 1; if (depth === 0) { return { start, end: index + 1, value: value.slice(start + 2, index).trim(), }; } } } return null; }; const createFootnoteChildren = (value) => { const tree = fromMarkdown(value); if (tree.children.length > 0) { return tree.children; } return [ { type: "paragraph", children: [ { type: "text", value, }, ], }, ]; }; const createFootnoteParagraph = (children) => ({ type: "paragraph", children: children.length > 0 ? children : [ { type: "text", value: "", }, ], }); const createFootnoteDefinition = (identifier, label, value) => ({ type: "footnoteDefinition", identifier, label, children: createFootnoteChildren(value), }); const createFootnoteDefinitionFromChildren = (identifier, label, children) => ({ type: "footnoteDefinition", identifier, label, children: [createFootnoteParagraph(children)], }); const createFootnoteReference = (context) => { context.count += 1; const label = String(context.count); const identifier = `${INLINE_FOOTNOTE_PREFIX}-${label}`; return { identifier, label, node: { type: "footnoteReference", identifier, label, }, }; }; const findClosingBracket = (value) => { for (let index = 0; index < value.length; index += 1) { const character = value[index]; if (character === "\\" && index + 1 < value.length) { index += 1; continue; } if (character === "]") { return index; } } return -1; }; const cloneNode = (node) => JSON.parse(JSON.stringify(node)); const splitTextNode = (node, context) => { const replacements = []; let cursor = 0; let match = findInlineFootnote(node.value, cursor); while (match) { if (match.start > cursor) { replacements.push({ type: "text", value: node.value.slice(cursor, match.start), }); } if (match.value) { const reference = createFootnoteReference(context); replacements.push(reference.node); context.definitions.push( createFootnoteDefinition(reference.identifier, reference.label, match.value), ); } else { replacements.push({ type: "text", value: node.value.slice(match.start, match.end), }); } cursor = match.end; match = findInlineFootnote(node.value, cursor); } if (cursor < node.value.length) { replacements.push({ type: "text", value: node.value.slice(cursor), }); } return replacements; }; const splitAcrossInlineNodes = (children, startIndex, context) => { const startNode = children[startIndex]; const start = startNode.value.indexOf("^["); if (start === -1) { return null; } if (findInlineFootnote(startNode.value, start)) { return null; } const footnoteChildren = []; const replacements = []; const before = startNode.value.slice(0, start); const initialText = startNode.value.slice(start + 2); if (before) { replacements.push({ type: "text", value: before, }); } if (initialText) { footnoteChildren.push({ type: "text", value: initialText, }); } for (let index = startIndex + 1; index < children.length; index += 1) { const child = children[index]; if (child.type === "text") { const closingIndex = findClosingBracket(child.value); if (closingIndex !== -1) { const footnoteText = child.value.slice(0, closingIndex); const after = child.value.slice(closingIndex + 1); if (footnoteText) { footnoteChildren.push({ type: "text", value: footnoteText, }); } const reference = createFootnoteReference(context); replacements.push(reference.node); if (after) { replacements.push({ type: "text", value: after, }); } context.definitions.push( createFootnoteDefinitionFromChildren( reference.identifier, reference.label, footnoteChildren, ), ); return { consumed: index - startIndex + 1, replacements, }; } footnoteChildren.push(cloneNode(child)); continue; } footnoteChildren.push(cloneNode(child)); } return null; }; const visitChildren = (node, context) => { if (!Array.isArray(node.children)) { return; } for (let index = 0; index < node.children.length; index += 1) { const child = node.children[index]; if (child.type === "text" && child.value.includes("^[")) { const acrossNodes = splitAcrossInlineNodes(node.children, index, context); if (acrossNodes) { node.children.splice(index, acrossNodes.consumed, ...acrossNodes.replacements); index += acrossNodes.replacements.length - 1; continue; } const replacements = splitTextNode(child, context); node.children.splice(index, 1, ...replacements); index += replacements.length - 1; continue; } visitChildren(child, context); } }; const remarkInlineFootnotes = () => (tree) => { const context = { count: 0, definitions: [], }; visitChildren(tree, context); if (context.definitions.length > 0) { tree.children.push(...context.definitions); } }; export default remarkInlineFootnotes;