@peaceroad/markdown-it-footnote-here
v0.8.0
Published
A markdown-it plugin that renders footnotes near their definitions and collects prefixed notes as endnotes.
Downloads
425
Readme
markdown-it-footnote-here
This is a markdown-it plugin and a further fork of markdown-it-footnote-here, which is itself a fork of the original markdown-it-footnote plugin.
This plugin inserts footnotes just below paragraphs.
The input Markdown and the output HTML are as follows.
Markdown:
A paragraph.[^1]
[^1]: A footnote.
A paragraph.HTML:
<p>A paragraph.<a href="#fn1" id="fn-ref1" class="fn-noteref" role="doc-noteref">[1]</a></p>
<aside id="fn1" class="fn" role="doc-footnote">
<p><a href="#fn-ref1" class="fn-backlink" role="doc-backlink">[1]</a> A footnote.</p>
</aside>
<p>A paragraph.</p>Notice.
- When multiple instances of the same footnote number appear in the main content, the default behavior is that the backlink from the footnote will refer to the first instance.
- Endnotes default to a trailing backlink style, and repeated endnote references render backlinks for all occurrences with visible
a,b,c, ... suffix markers. - When the same footnote/endnote label is defined multiple times, behavior is controlled by
duplicates.policy(default:warn). - When a note starts or ends with a non-paragraph block such as a list, heading, blockquote, or fence, the plugin inserts standalone label/backlink paragraphs as needed so numbering and return links remain visible.
- If an endnote definition is nested inside another note definition, endnotes are flattened into separate
<li>elements in note-number order so the generated<ol>remains structurally valid.
Endnotes
When a footnote label starts with endnotes.prefix (default: en-), it is collected at the end of the document and rendered as endnotes. This is a source-only classification marker: it is not used as the visible label or DOM prefix. As with ordinary named footnotes, the remainder of the source label identifies the note but does not determine its displayed number. The reference/backlink label for endnotes uses references.endnote.prefix (default: E), so endnotes appear as [E1], [E2], ... DOM ids/classes continue to use the fixed en prefix. By default, endnotes render the visible label at the start of the note body and the return link at the end.
Markdown:
A paragraph.[^en-1]
[^en-1]: An endnote.
A paragraph.HTML:
<p>A paragraph.<a href="#en1" id="en-ref1" class="en-noteref" role="doc-noteref">[E1]</a></p>
<p>A paragraph.</p>
<section aria-label="Notes" id="endnotes" role="doc-endnotes">
<ol>
<li id="en1">
<p><span class="en-label">[E1]</span> An endnote. <a href="#en-ref1" class="en-backlink" role="doc-backlink" aria-label="Back to reference E1">↩</a></p>
</li>
</ol>
</section>Use
import mdit from 'markdown-it'
import mditFootnoteHere from '@peaceroad/markdown-it-footnote-here'
const md = mdit().use(mditFootnoteHere)
md.render(/*...*/) // See examples aboveNote grammar API
Integrations that need footnote-style markers without this plugin's document-scoped numbering, placement, IDs, backlinks, or HTML can import the browser-safe pure scanners directly:
import {
scanNoteDefinitionMarker,
scanNoteReferenceMarker,
} from '@peaceroad/markdown-it-footnote-here/note-grammar.js'Installing this package or importing this subpath does not activate the footnote plugin. The plugin is registered only when its default export is passed to md.use(...). Companion plugins can therefore depend on these scanners while leaving ordinary footnotes unhandled or delegating them to another footnote plugin.
scanNoteReferenceMarker(src, start, max) recognizes [^label] beginning exactly at start. For example, scanNoteReferenceMarker('See it: [^tn-1].', 8) returns a fresh result object:
{
label: 'tn-1',
start: 8,
end: 15,
labelStart: 10,
labelEnd: 14,
}scanNoteDefinitionMarker(src, start, max) recognizes [^label]: beginning exactly at start. For example, scanNoteDefinitionMarker('[^tn-1]: text', 0) returns:
{
label: 'tn-1',
start: 0,
markerEnd: 8,
contentStart: 9,
labelStart: 2,
labelEnd: 6,
}src must be a string, and start and a supplied max must be integers. max is an optional exclusive upper bound: the scanners inspect only the [start, max) range. It defaults to src.length, and larger values are clamped to src.length. Invalid arguments or ranges return null.
All offsets are UTF-16 code-unit offsets. end points immediately after a reference marker, markerEnd points immediately after the definition colon, and contentStart points after any following space/tab run. labelEnd and these other end positions are exclusive. The scanners return null for non-matches, do not mutate their arguments or parser state, and have no dependency on markdown-it, tokens, env, locales, renderers, or Node.js APIs. Each successful call returns a fresh object that callers should treat as immutable.
The scanners intentionally preserve this plugin's existing marker grammar:
- Labels are non-empty and end at the first
]. - Code units U+0000 through U+0020 are rejected inside labels. Other Unicode whitespace and control characters are not newly rejected by this API.
- Label escapes, case folding, Unicode normalization, and prefix interpretation are not performed.
- Definition markers require
:immediately after]. contentStartskips only U+0020 SPACE and U+0009 TAB after the definition marker, within the supplied range.
These helpers only recognize marker boundaries. A consuming plugin remains responsible for rule ownership, definition bodies, scope, counters, IDs, backlinks, diagnostics, and output. No label prefix such as tn- or table- is reserved by this package.
Install
npm install @peaceroad/markdown-it-footnote-hereOptions
- Breaking change: options are now grouped into
references,backlinks,endnotes, andduplicates. - Removed legacy keys now throw at plugin setup. This includes the old top-level keys such as
beforeSameBacklink,afterBacklink*,labelBra,labelKet,labelSupTag,backLabelBra,backLabelKet,endnotesPrefix,endnotesLabelPrefix,endnotesSection*,duplicateDefinition*,injectErrorStyle, and the old sharedbacklinks.content,backlinks.duplicateMarker,backlinks.trailingLabel,backlinks.ariaLabelPrefix. references(object): controls how note references render in main text.footnote.prefix/endnote.prefix: visible label prefix before the note number. Defaults are''for footnotes and'E'for endnotes.footnote.brackets.open/.close,endnote.brackets.open/.close: visible brackets around note reference labels. Default:'['and']'.footnote.wrapInSup/endnote.wrapInSup: whether that kind of note reference is wrapped in<sup>. Default:false.
backlinks(object): controls labels and return links inside note bodies.footnote.position/endnote.position:'before' | 'after' | 'both' | 'none'(defaults: footnotes'before', endnotes'after').footnote.duplicates/endnote.duplicates:'first' | 'all'(defaults: footnotes'first', endnotes'all').allrenders one backlink per repeated reference;firstpoints back only to the first occurrence.footnote.brackets.open/.close,endnote.brackets.open/.close: visible brackets used for leading note labels/backlinks. Default:'['and']'.footnote.content/endnote.content: trailing backlink content. Default:'↩'.footnote.duplicateMarker/endnote.duplicateMarker: visible duplicate suffix style,'alpha' | 'numeric'(default:'alpha').footnote.trailingLabel/endnote.trailingLabel: whether trailing backlinks render a visible duplicate suffix marker,'none' | 'marker'(defaults: footnotes'none', endnotes'marker').footnote.ariaLabelPrefix/endnote.ariaLabelPrefix: prefix for trailing backlinkaria-labelvalues. Default: locale-aware auto. Built-in defaults are English'Back to reference 'and Japanese'元の参照に戻る '.- Note: when
positionincludesbeforeandduplicatesisall, duplicate references and leading backlinks use visible suffixes such asa,b,c(or1,2,3whenduplicateMarker: 'numeric'). - Note: default endnotes use trailing backlinks with visible suffix markers, so repeated endnote references render return links such as
↩a,↩b,↩c. - Note:
position: 'none'still renders a non-link note label so the note number remains visible.
endnotes(object): controls how endnotes are detected and where they are rendered.prefix: prefix that marks a note as an endnote (default:'en-'). When empty, endnotes are disabled.section.id:idattribute for the endnotes section wrapper; omitted when empty (default:'endnotes').section.className:classattribute for the endnotes section wrapper; omitted when empty (default:'').section.label: used asaria-labelwhensection.useHeadingisfalse; used as heading text whensection.useHeadingistrue. Default: locale-aware auto. Built-in defaults are English'Notes'and Japanese'後注'.section.useHeading: iftrue, render a heading tag and omitaria-label. Iffalse(default), omit the heading and setaria-labelwhen provided.section.headingLevel: heading level used whensection.useHeadingistrue, limited to1..6(default:2).
duplicates(object): controls duplicate note-definition handling.policy:'warn' | 'ignore' | 'strict'(default:'warn').'warn': keep first definition, mark note block withfootnote-error, mark backlinks withfootnote-error-backlink, and prepend<span class="footnote-error-message">...</span>in note content.'ignore': keep first definition and do not add warning classes/messages.'strict': throw an error on duplicate label.message: warning text used whenpolicyis'warn'(default:'[Duplicate footnote label detected. Using the first definition.]').injectStyle: iftrueandpolicyis'warn', inject a<style>block once per document for.footnote-error-messageand.footnote-error-backlink(includesprefers-color-schemeandforced-colorshandling). Default:false.
- Example:
const md = mdit().use(mditFootnoteHere, {
references: {
footnote: {
brackets: { open: '[', close: ']' },
},
endnote: {
prefix: 'E',
brackets: { open: '[', close: ']' },
},
},
backlinks: {
footnote: { position: 'before', duplicates: 'first' },
endnote: { position: 'after', duplicates: 'all', content: '↑', trailingLabel: 'marker' },
},
endnotes: {
prefix: 'en-',
section: { label: 'Notes', useHeading: true, headingLevel: 2 },
},
duplicates: {
policy: 'warn',
},
})- Diagnostics: when duplicates are detected under
duplicates.policy: 'warn', details are collected inenv.footnoteHereDiagnostics.duplicateDefinitions. - Security note: option strings used in HTML output are escaped before rendering (labels, aria/id/class values, heading text, backlink content/message).
- Locale note: when
backlinks.<kind>.ariaLabelPrefixorendnotes.section.labelisnullorundefined, the plugin uses built-in locale-aware defaults based onenv.locale,env.preferredLocales[0], compatibility fallbacksenv.lang,env.language,env.preferredLanguage,env.preferredLanguages[0], then English as the final fallback. - Locale note: localized defaults are resolved during the full markdown-it parse/core pass. If the locale changes, call
md.render(src, env)again or re-parse the source;renderer.render(existingTokens, ...)does not retroactively rewrite localized HTML tokens that were already generated during core transforms. - Locale note: explicit strings always win over locale-aware defaults. Use
''to disable the built-in default text instead of inheriting it. - Unreferenced note-definition note: when a note definition has no matching reference, the note still renders its visible label, but backlink anchors are omitted so broken
hreftargets are not emitted. env.docIdnote: if provided, it is URL-encoded and applied consistently to note/ref ids to keep links valid and safe. Ill-formed lone UTF-16 surrogates are replaced with U+FFFD before encoding instead of aborting the render.- Reused-render note: repeated renders are stable because duplicate-reference suffixes are fixed during parse, and changing
env.docIdon a reusedenvobject updates generated ids consistently. - Renderer note:
footnote_refandfootnote_openboth resolve note target ids through renderer rulefootnote_anchor_name, so custom note-anchor-name overrides keep referencehrefvalues and rendered note ids aligned. - Renderer note: hosts may call render paths without a populated
env(for example, inline-only renders). In that case, references render without count-based suffixes; pass a sharedenvduring parse+render to keep counts consistent. - Installation note: applying this plugin more than once to the same markdown-it instance is not supported and throws at setup time. Create a separate markdown-it instance for a different option set.
