marked-footnote
Version:
A marked extension to support GFM footnotes
1 lines • 12.8 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/footnote.ts","../src/references.ts","../src/footnotes.ts","../src/index.ts"],"sourcesContent":["import type { TokenizerAndRendererExtension, TokenizerThis } from 'marked'\nimport type { Footnote, Footnotes, LexerTokens } from './types.js'\n\n/**\n * Returns an extension object for parsing footnote definitions.\n */\nexport function createFootnote(lexer: LexerTokens, description: string) {\n const footnotes: Footnotes = {\n type: 'footnotes',\n raw: description,\n rawItems: [],\n items: []\n }\n\n return {\n name: 'footnote',\n level: 'block',\n childTokens: ['content'],\n tokenizer(this: TokenizerThis, src: string) {\n if (!lexer.hasFootnotes) {\n this.lexer.tokens.push(footnotes)\n\n lexer.tokens = this.lexer.tokens\n lexer.hasFootnotes = true\n\n // always begin with empty items\n footnotes.rawItems = []\n footnotes.items = []\n }\n\n const match =\n /^\\[\\^([^\\]\\n]+)\\]:(?:[ \\t]+|[\\n]*?|$)([^\\n]*?(?:\\n|$)(?:\\n*?[ ]{4,}[^\\n]*)*)/.exec(\n src\n )\n\n if (match) {\n const [raw, label, text = ''] = match\n let content = text.split('\\n').reduce((acc, curr) => {\n return acc + '\\n' + curr.replace(/^(?:[ ]{4}|[\\t])/, '')\n }, '')\n\n const contentLastLine = content.trimEnd().split('\\n').pop()\n\n content +=\n // add lines after list, blockquote, codefence, and table\n contentLastLine &&\n /^[ \\t]*?[>\\-*][ ]|[`]{3,}$|^[ \\t]*?[|].+[|]$/.test(contentLastLine)\n ? '\\n\\n'\n : ''\n\n const token: Footnote = {\n type: 'footnote',\n raw,\n label,\n refs: [],\n content: this.lexer.blockTokens(content)\n }\n\n footnotes.rawItems.push(token)\n\n return token\n }\n },\n renderer() {\n // skip it for now!\n // we will render all `Footnote` through the footnotes renderer\n return ''\n }\n } as TokenizerAndRendererExtension\n}\n","import type { TokenizerAndRendererExtension, TokenizerThis } from 'marked'\nimport type { FootnoteRef, Footnotes } from './types.js'\n\n/**\n * Escapes special HTML characters in a text to be inserted to an element body.\n */\nfunction escapeTextContent(text: string): string {\n return text\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n}\n\n/**\n * Returns an extension object for parsing inline footnote references.\n */\nexport function createFootnoteRef(\n prefixId: string,\n refMarkers = false,\n keepLabels = false\n) {\n let order = 0\n\n return {\n name: 'footnoteRef',\n level: 'inline',\n tokenizer(this: TokenizerThis, src: string) {\n const match = /^\\[\\^([^\\]\\n]+)\\]/.exec(src)\n\n if (match) {\n const [raw, label] = match\n const footnotes = this.lexer.tokens[0] as Footnotes\n const filteredRawItems = footnotes.rawItems.filter(\n item => item.label === label\n )\n\n if (!filteredRawItems.length) return\n\n const rawFootnote = filteredRawItems[0]\n const footnote = footnotes.items.filter(item => item.label === label)[0]\n\n const ref: FootnoteRef = {\n type: 'footnoteRef',\n raw,\n index: rawFootnote.refs.length,\n id: '',\n label\n }\n\n if (footnote) {\n ref.id = footnote.refs[0].id\n footnote.refs.push(ref)\n } else {\n order++\n ref.id = String(order)\n rawFootnote.refs.push(ref)\n footnotes.items.push(rawFootnote)\n }\n\n return ref\n }\n },\n renderer({ index, id, label }: FootnoteRef) {\n order = 0 // reset order\n const encodedLabel = encodeURIComponent(label)\n const textLabel = keepLabels ? escapeTextContent(label) : id\n const idSuffix = index > 0 ? `-${index + 1}` : ''\n\n return `<sup><a id=\"${prefixId}ref-${encodedLabel}${idSuffix}\" href=\"#${\n prefixId + encodedLabel\n }\" data-${prefixId}ref aria-describedby=\"${prefixId}label\">${\n refMarkers ? `[${textLabel}]` : textLabel\n }</a></sup>`\n }\n } as TokenizerAndRendererExtension\n}\n","import type { RendererExtension, RendererThis } from 'marked'\nimport type { Footnotes } from './types.js'\n\n/**\n * Returns an extension object for rendering the list of footnotes.\n */\nexport function createFootnotes(\n prefixId: string,\n prefixData: string,\n footnoteDivider: boolean,\n sectionClass: string,\n headingClass: string,\n backRefLabel: string\n) {\n return {\n name: 'footnotes',\n renderer(this: RendererThis, { raw, items = [] }: Footnotes) {\n if (items.length === 0) return ''\n\n const footnotesItemsHTML = items.reduce(\n (acc, { label, content, refs }) => {\n const encodedLabel = encodeURIComponent(label)\n const parsedContent = this.parser.parse(content).trimEnd()\n const isEndsWithP = parsedContent.endsWith('</p>')\n\n let footnoteItem = `<li id=\"${prefixId + encodedLabel}\">\\n`\n footnoteItem += isEndsWithP\n ? parsedContent.replace(/<\\/p>$/, '')\n : parsedContent\n\n refs.forEach((_, i) => {\n const ariaLabel = backRefLabel.replace('{0}', label)\n let textLabel: string\n let idSuffix: string\n if (i > 0) {\n const displayIndex = i + 1\n textLabel = `↩<sup>${displayIndex}</sup>`\n idSuffix = `-${displayIndex}`\n } else {\n textLabel = '↩'\n idSuffix = ''\n }\n footnoteItem += ` <a href=\"#${prefixId}ref-${encodedLabel}${idSuffix}\" data-${prefixId}backref aria-label=\"${ariaLabel}\">${textLabel}</a>`\n })\n\n footnoteItem += isEndsWithP ? '</p>\\n' : '\\n'\n footnoteItem += '</li>\\n'\n\n return acc + footnoteItem\n },\n ''\n )\n\n let footnotesHTML = ''\n if (footnoteDivider) {\n footnotesHTML += `<hr data-${prefixData}footnotes>\\n`\n }\n let sectionAttrs = ''\n if (sectionClass) {\n sectionAttrs = ` class=\"${sectionClass}\"`\n }\n let headingAttrs = ''\n if (headingClass) {\n headingAttrs = ` class=\"${headingClass}\"`\n }\n footnotesHTML += `<section${sectionAttrs} data-${prefixData}footnotes>\\n`\n footnotesHTML += `<h2 id=\"${prefixId}label\"${headingAttrs}>${raw.trimEnd()}</h2>\\n`\n footnotesHTML += `<ol>\\n${footnotesItemsHTML}</ol>\\n`\n footnotesHTML += '</section>\\n'\n\n return footnotesHTML\n }\n } as RendererExtension\n}\n","import type { MarkedExtension } from 'marked'\nimport { createFootnote } from './footnote.js'\nimport { createFootnoteRef } from './references.js'\nimport { createFootnotes } from './footnotes.js'\nimport type { LexerTokens, Options } from './types.js'\n\n/**\n * A [marked](https://marked.js.org/) extension to support [GFM footnotes](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes).\n */\nexport default function markedFootnote(options: Options = {}): MarkedExtension {\n const {\n prefixId = 'footnote-',\n prefixData = '',\n description = 'Footnotes',\n refMarkers = false,\n footnoteDivider = false,\n keepLabels = false,\n sectionClass = 'footnotes',\n headingClass = 'sr-only',\n backRefLabel = 'Back to reference {0}'\n } = options\n const lexer: LexerTokens = { hasFootnotes: false, tokens: [] }\n\n return {\n extensions: [\n createFootnote(lexer, description),\n createFootnoteRef(prefixId, refMarkers, keepLabels),\n createFootnotes(\n prefixId,\n prefixData,\n footnoteDivider,\n sectionClass,\n headingClass,\n backRefLabel\n )\n ],\n walkTokens(token) {\n if (\n token.type === 'footnotes' &&\n lexer.tokens.indexOf(token) === 0 &&\n token.items.length\n ) {\n lexer.tokens[0] = { type: 'space', raw: '' }\n lexer.tokens.push(token)\n }\n\n if (lexer.hasFootnotes) lexer.hasFootnotes = false\n }\n }\n}\n\nexport type { Footnote, FootnoteRef, Footnotes, Options } from './types.js'\n"],"names":["createFootnote","lexer","description","footnotes","src","match","raw","label","text","content","acc","curr","contentLastLine","token","escapeTextContent","createFootnoteRef","prefixId","refMarkers","keepLabels","order","filteredRawItems","item","rawFootnote","footnote","ref","index","id","encodedLabel","textLabel","idSuffix","createFootnotes","prefixData","footnoteDivider","sectionClass","headingClass","backRefLabel","items","footnotesItemsHTML","refs","parsedContent","isEndsWithP","footnoteItem","_","i","ariaLabel","displayIndex","footnotesHTML","sectionAttrs","headingAttrs","markedFootnote","options"],"mappings":"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"}