Something went wrong. Try again.
My blog
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324// deno-lint-ignore-file no-explicit-any
export interface Options { /** Key to save the title in the page data */ key: string;
/** The prefix to assign to all ids */ idPrefix: string;
/** The prefix to assign to all references ids */ referenceIdPrefix: string;
/** HTML attributes to the <sup> element used for the reference */ referenceAttrs: Record<string, string>;}
export const defaults: Options = { key: "footnotes", idPrefix: "fn-", referenceIdPrefix: "fnref-", referenceAttrs: { class: "fn-ref", },};
export default function footNotes(md: any, userOptions: Partial<Options> = {}) { const options = Object.assign({}, defaults, userOptions) as Options; const parseLinkLabel = md.helpers.parseLinkLabel; const isSpace = md.utils.isSpace;
md.renderer.rules.footnote_reference = function (tokens: any[], idx: number) { const { id, label } = tokens[idx].meta; const attrs = Object.entries(options.referenceAttrs) .map(([key, value]) => `${key}="${value}"`);
attrs.push(`href="#${options.idPrefix}${id}"`); attrs.push(`id="${options.referenceIdPrefix}${id}"`);
return `<sup><a ${attrs.join(" ")}>${id}</a></sup>`; };
// Process footnote block definition function footnote_block( state: any, startLine: number, endLine: number, silent: boolean, ) { const start = state.bMarks[startLine] + state.tShift[startLine]; const max = state.eMarks[startLine];
/* Line should be at least 5 characters: [^x]: */ if ( start + 4 > max || state.src.charCodeAt(start) !== 0x5B || /* [ */ state.src.charCodeAt(start + 1) !== 0x5E /* ^ */ ) { return false; }
let pos;
for (pos = start + 2; pos < max; pos++) { if (state.src.charCodeAt(pos) === 0x20) { return false; } if (state.src.charCodeAt(pos) === 0x5D /* ] */) { break; } }
// no empty footnote labels if (pos === start + 2) { return false; }
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) { return false; }
if (silent) { return true; }
pos++;
const footnotes = getFootnotes(state); const label = state.src.slice(start + 2, pos - 2); const id = footnotes.size + 1;
const openToken = new state.Token("footnote_reference_open", "", 1); openToken.meta = { id }; openToken.level = state.level++; state.tokens.push(openToken);
footnotes.set(id, { id, label });
const oldBMark = state.bMarks[startLine]; const oldTShift = state.tShift[startLine]; const oldSCount = state.sCount[startLine]; const oldParentType = state.parentType;
const posAfterColon = pos; const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]); let offset = initial;
while (pos < max) { const ch = state.src.charCodeAt(pos);
if (!isSpace(ch)) { break; } if (ch === 0x09) { offset += 4 - offset % 4; } else { offset++; }
pos++; }
state.tShift[startLine] = pos - posAfterColon; state.sCount[startLine] = offset - initial; state.bMarks[startLine] = posAfterColon; state.blkIndent += 4; state.parentType = "footnote";
if (state.sCount[startLine] < state.blkIndent) { state.sCount[startLine] += state.blkIndent; }
state.md.block.tokenize(state, startLine, endLine, true);
state.parentType = oldParentType; state.blkIndent -= 4; state.tShift[startLine] = oldTShift; state.sCount[startLine] = oldSCount; state.bMarks[startLine] = oldBMark;
const closeToken = new state.Token("footnote_reference_close", "", -1); closeToken.level = --state.level; state.tokens.push(closeToken);
return true; }
// Process inline footnotes (^[...]) function footnote_inline(state: any, silent: boolean) { const max = state.posMax; const start = state.pos;
/* Line should be at least 2 characters: ^[ */ if ( start + 2 >= max || state.src.charCodeAt(start) !== 0x5E || state.src.charCodeAt(start + 1) !== 0x5B ) { return false; }
const labelStart = start + 2; const labelEnd = parseLinkLabel(state, start + 1);
// parser failed to find ']', so it's not a valid note if (labelEnd < 0) { return false; }
// We found the end of the link, and know for a fact it's a valid link; // so all that's left to do is to call tokenizer. // if (!silent) { const footnotes = getFootnotes(state); const id = footnotes.size + 1; const label = id.toString();
const token = state.push("footnote_reference", "", 0); token.meta = { id, label };
footnotes.set(id, { id, label, content: `${state.src.slice(labelStart, labelEnd)}`, }); }
state.pos = labelEnd + 1; state.posMax = max; return true; }
// Process footnote references ([^...]) function footnote_reference(state: any, silent: boolean) { const max = state.posMax; const start = state.pos;
/* Line should be at least 4 characters: [^x] */ if ( start + 3 > max || state.src.charCodeAt(start) !== 0x5B /* [ */ || state.src.charCodeAt(start + 1) !== 0x5E /* ^ */ ) { return false; }
const labelStart = start + 2; let labelEnd = 0;
for (labelEnd = labelStart; labelEnd < max; labelEnd++) { const char = state.src.charCodeAt(labelEnd);
if (char === 0x20 || char === 0x0A) { return false; }
if (char === 0x5D /* ] */) { break; } }
if (labelStart === labelEnd || labelEnd >= max) { return false; }
if (!silent) { const label = state.src.slice(labelStart, labelEnd); const footnote = searchFootnote(state, label); const token = state.push("footnote_reference", "", 0); token.meta = { ...footnote }; }
state.pos = ++labelEnd; state.posMax = max; return true; }
// Glue footnote tokens to end of token stream function footnote_tail(state: any) { const footnotes = getFootnotes(state);
if (!footnotes.size) { return; }
let currentFootnote: Footnote | undefined; let currentTokens: any[] | undefined;
state.tokens = state.tokens.filter(function (tok: any) { if (tok.type === "footnote_reference_open") { currentFootnote = footnotes.get(tok.meta.id)!; currentTokens = []; return false; }
if (tok.type === "footnote_reference_close") { currentFootnote!.content = md.renderer.render( currentTokens, state.md.options, state.env, ); currentTokens = undefined; return false; }
if (currentTokens) { currentTokens.push(tok); }
return !currentTokens; }); }
md.block.ruler.before("reference", "footnote_block", footnote_block, { alt: ["paragraph", "reference"], }); md.inline.ruler.after("image", "footnote_inline", footnote_inline); md.inline.ruler.after("footnote_inline", "footnote_reference", footnote_reference); md.core.ruler.after("inline", "footnote_tail", footnote_tail);
md.core.ruler.push("saveFootnotes", function (state: any) { const data = state.env.data?.page?.data;
if (!data || data[options.key]) { return; }
const footnotes = getFootnotes(state); data[options.key] = Array.from(footnotes.values()).map((footnote) => ({ id: `${options.idPrefix}${footnote.id}`, refId: `${options.referenceIdPrefix}${footnote.id}`, label: footnote.label, content: footnote.content, rawId: `${footnote.id}`, })); });}
interface Footnote { id: number; label?: string; content?: string; tokens?: any[]; rawId?: string;}
function getFootnotes(state: any): Map<number, Footnote> { if (!state.env.fn) { state.env.fn = new Map<number, Footnote>(); }
return state.env.fn;}
function searchFootnote(state: any, label: string) { const map = getFootnotes(state);
for (const value of map.values()) { if (value.label === label) { return value; } }}