marked-footnote
Version:
A marked extension to support GFM footnotes
1 lines • 11.5 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 * Returns an extension object for parsing inline footnote references.\n */\nexport function createFootnoteRef(prefixId: string, refMarkers = false) {\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 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({ id, label }: FootnoteRef) {\n order = 0 // reset order\n const encodedLabel = encodeURIComponent(label)\n\n return `<sup><a id=\"${prefixId}ref-${encodedLabel}\" href=\"#${\n prefixId + encodedLabel\n }\" data-${prefixId}ref aria-describedby=\"${prefixId}label\">${\n refMarkers ? `[${id}]` : id\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 footnoteItem += ` <a href=\"#${prefixId}ref-${encodedLabel}\" data-${prefixId}backref aria-label=\"${ariaLabel}\">${\n i > 0 ? `↩<sup>${i + 1}</sup>` : '↩'\n }</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 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),\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","createFootnoteRef","prefixId","refMarkers","order","filteredRawItems","item","rawFootnote","footnote","ref","id","encodedLabel","createFootnotes","prefixData","footnoteDivider","sectionClass","headingClass","backRefLabel","items","footnotesItemsHTML","refs","parsedContent","isEndsWithP","footnoteItem","_","i","ariaLabel","footnotesHTML","sectionAttrs","headingAttrs","markedFootnote","options"],"mappings":"aAMgB,SAAAA,EAAeC,EAAoBC,EAAqB,CACtE,MAAMC,EAAuB,CAC3B,KAAM,YACN,IAAKD,EACL,SAAU,CAAC,EACX,MAAO,CAAA,CACT,EAEO,MAAA,CACL,KAAM,WACN,MAAO,QACP,YAAa,CAAC,SAAS,EACvB,UAA+BE,EAAa,CACrCH,EAAM,eACJ,KAAA,MAAM,OAAO,KAAKE,CAAS,EAE1BF,EAAA,OAAS,KAAK,MAAM,OAC1BA,EAAM,aAAe,GAGrBE,EAAU,SAAW,CAAC,EACtBA,EAAU,MAAQ,CAAC,GAGrB,MAAME,EACJ,+EAA+E,KAC7ED,CACF,EAEF,GAAIC,EAAO,CACT,KAAM,CAACC,EAAKC,EAAOC,EAAO,EAAE,EAAIH,EAC5B,IAAAI,EAAUD,EAAK,MAAM;AAAA,CAAI,EAAE,OAAO,CAACE,EAAKC,IACnCD,EAAM;AAAA,EAAOC,EAAK,QAAQ,mBAAoB,EAAE,EACtD,EAAE,EAEL,MAAMC,EAAkBH,EAAQ,QAAA,EAAU,MAAM;AAAA,CAAI,EAAE,IAAI,EAE1DA,GAEEG,GACA,+CAA+C,KAAKA,CAAe,EAC/D;AAAA;AAAA,EACA,GAEN,MAAMC,EAAkB,CACtB,KAAM,WACN,IAAAP,EACA,MAAAC,EACA,KAAM,CAAC,EACP,QAAS,KAAK,MAAM,YAAYE,CAAO,CACzC,EAEU,OAAAN,EAAA,SAAS,KAAKU,CAAK,EAEtBA,CAAA,CAEX,EACA,UAAW,CAGF,MAAA,EAAA,CAEX,CACF,CC/DgB,SAAAC,EAAkBC,EAAkBC,EAAa,GAAO,CACtE,IAAIC,EAAQ,EAEL,MAAA,CACL,KAAM,cACN,MAAO,SACP,UAA+Bb,EAAa,CACpC,MAAAC,EAAQ,oBAAoB,KAAKD,CAAG,EAE1C,GAAIC,EAAO,CACH,KAAA,CAACC,EAAKC,CAAK,EAAIF,EACfF,EAAY,KAAK,MAAM,OAAO,CAAC,EAC/Be,EAAmBf,EAAU,SAAS,OAC1CgB,GAAQA,EAAK,QAAUZ,CACzB,EAEI,GAAA,CAACW,EAAiB,OAAQ,OAExB,MAAAE,EAAcF,EAAiB,CAAC,EAChCG,EAAWlB,EAAU,MAAM,UAAegB,EAAK,QAAUZ,CAAK,EAAE,CAAC,EAEjEe,EAAmB,CACvB,KAAM,cACN,IAAAhB,EACA,GAAI,GACJ,MAAAC,CACF,EAEA,OAAIc,GACFC,EAAI,GAAKD,EAAS,KAAK,CAAC,EAAE,GACjBA,EAAA,KAAK,KAAKC,CAAG,IAEtBL,IACIK,EAAA,GAAK,OAAOL,CAAK,EACTG,EAAA,KAAK,KAAKE,CAAG,EACfnB,EAAA,MAAM,KAAKiB,CAAW,GAG3BE,CAAA,CAEX,EACA,SAAS,CAAE,GAAAC,EAAI,MAAAhB,GAAsB,CAC3BU,EAAA,EACF,MAAAO,EAAe,mBAAmBjB,CAAK,EAE7C,MAAO,eAAeQ,CAAQ,OAAOS,CAAY,YAC/CT,EAAWS,CACb,UAAUT,CAAQ,yBAAyBA,CAAQ,UACjDC,EAAa,IAAIO,CAAE,IAAMA,CAC3B,YAAA,CAEJ,CACF,CCpDO,SAASE,EACdV,EACAW,EACAC,EACAC,EACAC,EACAC,EACA,CACO,MAAA,CACL,KAAM,YACN,SAA6B,CAAE,IAAAxB,EAAK,MAAAyB,EAAQ,IAAiB,CACvD,GAAAA,EAAM,SAAW,EAAU,MAAA,GAE/B,MAAMC,EAAqBD,EAAM,OAC/B,CAACrB,EAAK,CAAE,MAAAH,EAAO,QAAAE,EAAS,KAAAwB,KAAW,CAC3B,MAAAT,EAAe,mBAAmBjB,CAAK,EACvC2B,EAAgB,KAAK,OAAO,MAAMzB,CAAO,EAAE,QAAQ,EACnD0B,EAAcD,EAAc,SAAS,MAAM,EAE7C,IAAAE,EAAe,WAAWrB,EAAWS,CAAY;AAAA,EACrD,OAAAY,GAAgBD,EACZD,EAAc,QAAQ,SAAU,EAAE,EAClCA,EAECD,EAAA,QAAQ,CAACI,EAAGC,IAAM,CACrB,MAAMC,EAAYT,EAAa,QAAQ,MAAOvB,CAAK,EACnD6B,GAAgB,cAAcrB,CAAQ,OAAOS,CAAY,UAAUT,CAAQ,uBAAuBwB,CAAS,KACzGD,EAAI,EAAI,SAASA,EAAI,CAAC,SAAW,GACnC,MAAA,CACD,EAEDF,GAAgBD,EAAc;AAAA,EAAW;AAAA,EACzBC,GAAA;AAAA,EAET1B,EAAM0B,CACf,EACA,EACF,EAEA,IAAII,EAAgB,GAChBb,IACFa,GAAiB,YAAYd,CAAU;AAAA,GAEzC,IAAIe,EAAe,GACfb,IACFa,EAAe,WAAWb,CAAY,KAExC,IAAIc,EAAe,GACnB,OAAIb,IACFa,EAAe,WAAWb,CAAY,KAEvBW,GAAA,WAAWC,CAAY,SAASf,CAAU;AAAA,EAC3Dc,GAAiB,WAAWzB,CAAQ,SAAS2B,CAAY,IAAIpC,EAAI,SAAS;AAAA,EACzDkC,GAAA;AAAA,EAASR,CAAkB;AAAA,EAC3BQ,GAAA;AAAA,EAEVA,CAAA,CAEX,CACF,CCxDwB,SAAAG,EAAeC,EAAmB,GAAqB,CACvE,KAAA,CACJ,SAAA7B,EAAW,YACX,WAAAW,EAAa,GACb,YAAAxB,EAAc,YACd,WAAAc,EAAa,GACb,gBAAAW,EAAkB,GAClB,aAAAC,EAAe,YACf,aAAAC,EAAe,UACf,aAAAC,EAAe,uBAAA,EACbc,EACE3C,EAAqB,CAAE,aAAc,GAAO,OAAQ,CAAA,CAAG,EAEtD,MAAA,CACL,WAAY,CACVD,EAAeC,EAAOC,CAAW,EACjCY,EAAkBC,EAAUC,CAAU,EACtCS,EACEV,EACAW,EACAC,EACAC,EACAC,EACAC,CAAA,CAEJ,EACA,WAAWjB,EAAO,CAEdA,EAAM,OAAS,aACfZ,EAAM,OAAO,QAAQY,CAAK,IAAM,GAChCA,EAAM,MAAM,SAEZZ,EAAM,OAAO,CAAC,EAAI,CAAE,KAAM,QAAS,IAAK,EAAG,EACrCA,EAAA,OAAO,KAAKY,CAAK,GAGrBZ,EAAM,eAAcA,EAAM,aAAe,GAAA,CAEjD,CACF"}