feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2014-2015 Vitaly Puzrin, Alex Kocharin.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+135
View File
@@ -0,0 +1,135 @@
# markdown-it-footnote
[![CI](https://github.com/markdown-it/markdown-it-footnote/actions/workflows/ci.yml/badge.svg)](https://github.com/markdown-it/markdown-it-footnote/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/markdown-it-footnote.svg?style=flat)](https://www.npmjs.org/package/markdown-it-footnote)
[![Coverage Status](https://img.shields.io/coveralls/markdown-it/markdown-it-footnote/master.svg?style=flat)](https://coveralls.io/r/markdown-it/markdown-it-footnote?branch=master)
> Footnotes plugin for [markdown-it](https://github.com/markdown-it/markdown-it) markdown parser.
__v2.+ requires `markdown-it` v5.+, see changelog.__
Markup is based on [pandoc](http://johnmacfarlane.net/pandoc/README.html#footnotes) definition.
__Normal footnote__:
```
Here is a footnote reference,[^1] and another.[^longnote]
[^1]: Here is the footnote.
[^longnote]: Here's one with multiple blocks.
Subsequent paragraphs are indented to show that they
belong to the previous footnote.
```
html:
```html
<p>Here is a footnote reference,<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> and another.<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup></p>
<p>This paragraph won’t be part of the note, because it
isn’t indented.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Here is the footnote. <a href="#fnref1" class="footnote-backref">↩</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Here’s one with multiple blocks.</p>
<p>Subsequent paragraphs are indented to show that they
belong to the previous footnote. <a href="#fnref2" class="footnote-backref">↩</a></p>
</li>
</ol>
</section>
```
__Inline footnote__:
```
Here is an inline note.^[Inlines notes are easier to write, since
you don't have to pick an identifier and move down to type the
note.]
```
html:
```html
<p>Here is an inline note.<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Inlines notes are easier to write, since
you don’t have to pick an identifier and move down to type the
note. <a href="#fnref1" class="footnote-backref">↩</a></p>
</li>
</ol>
</section>
```
## Install
node.js, browser:
```bash
npm install markdown-it-footnote --save
bower install markdown-it-footnote --save
```
## Use
```js
var md = require('markdown-it')()
.use(require('markdown-it-footnote'));
md.render(/*...*/) // See examples above
```
_Differences in browser._ If you load script directly into the page, without
package system, module will add itself globally as `window.markdownitFootnote`.
### Customize
If you want to customize the output, you'll need to replace the template
functions. To see which templates exist and their default implementations,
look in [`index.js`](index.js). The API of these template functions is out of
scope for this plugin's documentation; you can read more about it [in the
markdown-it
documentation](https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer).
To demonstrate with an example, here is how you might replace the `<hr>` that
this plugin emits by default with an `<h4>` emitted by your own template
function override:
```js
const md = require('markdown-it')().use(require('markdown-it-footnote'));
md.renderer.rules.footnote_block_open = () => (
'<h4 class="mt-3">Footnotes</h4>\n' +
'<section class="footnotes">\n' +
'<ol class="footnotes-list">\n'
);
```
Here's another example that customizes footnotes for epub books:
```js
const backrefLabel = 'back to text';
const epubRules = {
footnote_ref: ['<a', '<a epub:type="noteref"'],
footnote_open: ['<li', '<li epub:type="footnote"'],
footnote_anchor: ['<a', `<a aria-label="${backrefLabel}"`],
}
Object.keys(epubRules).map(rule => {
let defaultRender = md.renderer.rules[rule];
md.renderer.rules[rule] = (tokens, idx, options, env, self) => {
return defaultRender(tokens, idx, options, env, self).replace(...epubRules[rule]);
}
})
```
## License
[MIT](https://github.com/markdown-it/markdown-it-footnote/blob/master/LICENSE)
+304
View File
@@ -0,0 +1,304 @@
'use strict';
// Process footnotes
//
/// /////////////////////////////////////////////////////////////////////////////
// Renderer partials
function render_footnote_anchor_name(tokens, idx, options, env /*, slf */) {
const n = Number(tokens[idx].meta.id + 1).toString();
let prefix = '';
if (typeof env.docId === 'string') prefix = `-${env.docId}-`;
return prefix + n;
}
function render_footnote_caption(tokens, idx /*, options, env, slf */) {
let n = Number(tokens[idx].meta.id + 1).toString();
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
return `[${n}]`;
}
function render_footnote_ref(tokens, idx, options, env, slf) {
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
let refid = id;
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
}
function render_footnote_block_open(tokens, idx, options) {
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') + '<section class="footnotes">\n' + '<ol class="footnotes-list">\n';
}
function render_footnote_block_close() {
return '</ol>\n</section>\n';
}
function render_footnote_open(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
return `<li id="fn${id}" class="footnote-item">`;
}
function render_footnote_close() {
return '</li>\n';
}
function render_footnote_anchor(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`;
}
function footnote_plugin(md) {
const parseLinkLabel = md.helpers.parseLinkLabel;
const isSpace = md.utils.isSpace;
md.renderer.rules.footnote_ref = render_footnote_ref;
md.renderer.rules.footnote_block_open = render_footnote_block_open;
md.renderer.rules.footnote_block_close = render_footnote_block_close;
md.renderer.rules.footnote_open = render_footnote_open;
md.renderer.rules.footnote_close = render_footnote_close;
md.renderer.rules.footnote_anchor = render_footnote_anchor;
// helpers (only used in other rules, no tokens are attached to those)
md.renderer.rules.footnote_caption = render_footnote_caption;
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
// Process footnote block definition
function footnote_def(state, startLine, endLine, silent) {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
// line should be at least 5 chars - "[^x]:"
if (start + 4 > max) return false;
if (state.src.charCodeAt(start) !== 0x5B /* [ */) return false;
if (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;
}
}
if (pos === start + 2) return false; // no empty footnote labels
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) return false;
if (silent) return true;
pos++;
if (!state.env.footnotes) state.env.footnotes = {};
if (!state.env.footnotes.refs) state.env.footnotes.refs = {};
const label = state.src.slice(start + 2, pos - 2);
state.env.footnotes.refs[`:${label}`] = -1;
const token_fref_o = new state.Token('footnote_reference_open', '', 1);
token_fref_o.meta = {
label
};
token_fref_o.level = state.level++;
state.tokens.push(token_fref_o);
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)) {
if (ch === 0x09) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
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 token_fref_c = new state.Token('footnote_reference_close', '', -1);
token_fref_c.level = --state.level;
state.tokens.push(token_fref_c);
return true;
}
// Process inline footnotes (^[...])
function footnote_inline(state, silent) {
const max = state.posMax;
const start = state.pos;
if (start + 2 >= max) return false;
if (state.src.charCodeAt(start) !== 0x5E /* ^ */) return false;
if (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) {
if (!state.env.footnotes) state.env.footnotes = {};
if (!state.env.footnotes.list) state.env.footnotes.list = [];
const footnoteId = state.env.footnotes.list.length;
const tokens = [];
state.md.inline.parse(state.src.slice(labelStart, labelEnd), state.md, state.env, tokens);
const token = state.push('footnote_ref', '', 0);
token.meta = {
id: footnoteId
};
state.env.footnotes.list[footnoteId] = {
content: state.src.slice(labelStart, labelEnd),
tokens
};
}
state.pos = labelEnd + 1;
state.posMax = max;
return true;
}
// Process footnote references ([^...])
function footnote_ref(state, silent) {
const max = state.posMax;
const start = state.pos;
// should be at least 4 chars - "[^x]"
if (start + 3 > max) return false;
if (!state.env.footnotes || !state.env.footnotes.refs) return false;
if (state.src.charCodeAt(start) !== 0x5B /* [ */) return false;
if (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) === 0x0A) return false;
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
break;
}
}
if (pos === start + 2) return false; // no empty footnote labels
if (pos >= max) return false;
pos++;
const label = state.src.slice(start + 2, pos - 1);
if (typeof state.env.footnotes.refs[`:${label}`] === 'undefined') return false;
if (!silent) {
if (!state.env.footnotes.list) state.env.footnotes.list = [];
let footnoteId;
if (state.env.footnotes.refs[`:${label}`] < 0) {
footnoteId = state.env.footnotes.list.length;
state.env.footnotes.list[footnoteId] = {
label,
count: 0
};
state.env.footnotes.refs[`:${label}`] = footnoteId;
} else {
footnoteId = state.env.footnotes.refs[`:${label}`];
}
const footnoteSubId = state.env.footnotes.list[footnoteId].count;
state.env.footnotes.list[footnoteId].count++;
const token = state.push('footnote_ref', '', 0);
token.meta = {
id: footnoteId,
subId: footnoteSubId,
label
};
}
state.pos = pos;
state.posMax = max;
return true;
}
// Glue footnote tokens to end of token stream
function footnote_tail(state) {
let tokens;
let current;
let currentLabel;
let insideRef = false;
const refTokens = {};
if (!state.env.footnotes) {
return;
}
state.tokens = state.tokens.filter(function (tok) {
if (tok.type === 'footnote_reference_open') {
insideRef = true;
current = [];
currentLabel = tok.meta.label;
return false;
}
if (tok.type === 'footnote_reference_close') {
insideRef = false;
// prepend ':' to avoid conflict with Object.prototype members
refTokens[':' + currentLabel] = current;
return false;
}
if (insideRef) {
current.push(tok);
}
return !insideRef;
});
if (!state.env.footnotes.list) {
return;
}
const list = state.env.footnotes.list;
state.tokens.push(new state.Token('footnote_block_open', '', 1));
for (let i = 0, l = list.length; i < l; i++) {
const token_fo = new state.Token('footnote_open', '', 1);
token_fo.meta = {
id: i,
label: list[i].label
};
state.tokens.push(token_fo);
if (list[i].tokens) {
tokens = [];
const token_po = new state.Token('paragraph_open', 'p', 1);
token_po.block = true;
tokens.push(token_po);
const token_i = new state.Token('inline', '', 0);
token_i.children = list[i].tokens;
token_i.content = list[i].content;
tokens.push(token_i);
const token_pc = new state.Token('paragraph_close', 'p', -1);
token_pc.block = true;
tokens.push(token_pc);
} else if (list[i].label) {
tokens = refTokens[`:${list[i].label}`];
}
if (tokens) state.tokens = state.tokens.concat(tokens);
let lastParagraph;
if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
lastParagraph = state.tokens.pop();
} else {
lastParagraph = null;
}
const t = list[i].count > 0 ? list[i].count : 1;
for (let j = 0; j < t; j++) {
const token_a = new state.Token('footnote_anchor', '', 0);
token_a.meta = {
id: i,
subId: j,
label: list[i].label
};
state.tokens.push(token_a);
}
if (lastParagraph) {
state.tokens.push(lastParagraph);
}
state.tokens.push(new state.Token('footnote_close', '', -1));
}
state.tokens.push(new state.Token('footnote_block_close', '', -1));
}
md.block.ruler.before('reference', 'footnote_def', footnote_def, {
alt: ['paragraph', 'reference']
});
md.inline.ruler.after('image', 'footnote_inline', footnote_inline);
md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref);
md.core.ruler.after('inline', 'footnote_tail', footnote_tail);
}
module.exports = footnote_plugin;
@@ -0,0 +1,299 @@
/*! markdown-it-footnote 4.0.0 https://github.com/markdown-it/markdown-it-footnote @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitFootnote = factory());
})(this, (function() {
"use strict";
// Process footnotes
/// /////////////////////////////////////////////////////////////////////////////
// Renderer partials
function render_footnote_anchor_name(tokens, idx, options, env /*, slf */) {
const n = Number(tokens[idx].meta.id + 1).toString();
let prefix = "";
if (typeof env.docId === "string") prefix = `-${env.docId}-`;
return prefix + n;
}
function render_footnote_caption(tokens, idx /*, options, env, slf */) {
let n = Number(tokens[idx].meta.id + 1).toString();
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
return `[${n}]`;
}
function render_footnote_ref(tokens, idx, options, env, slf) {
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
let refid = id;
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
}
function render_footnote_block_open(tokens, idx, options) {
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') + '<section class="footnotes">\n' + '<ol class="footnotes-list">\n';
}
function render_footnote_block_close() {
return "</ol>\n</section>\n";
}
function render_footnote_open(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
return `<li id="fn${id}" class="footnote-item">`;
}
function render_footnote_close() {
return "</li>\n";
}
function render_footnote_anchor(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
/* ↩ with escape code to prevent display as Apple Emoji on iOS */;
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\ufe0e</a>`;
}
function footnote_plugin(md) {
const parseLinkLabel = md.helpers.parseLinkLabel;
const isSpace = md.utils.isSpace;
md.renderer.rules.footnote_ref = render_footnote_ref;
md.renderer.rules.footnote_block_open = render_footnote_block_open;
md.renderer.rules.footnote_block_close = render_footnote_block_close;
md.renderer.rules.footnote_open = render_footnote_open;
md.renderer.rules.footnote_close = render_footnote_close;
md.renderer.rules.footnote_anchor = render_footnote_anchor;
// helpers (only used in other rules, no tokens are attached to those)
md.renderer.rules.footnote_caption = render_footnote_caption;
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
// Process footnote block definition
function footnote_def(state, startLine, endLine, silent) {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
// line should be at least 5 chars - "[^x]:"
if (start + 4 > max) return false;
if (state.src.charCodeAt(start) !== 91 /* [ */) return false;
if (state.src.charCodeAt(start + 1) !== 94 /* ^ */) return false;
let pos;
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 32) return false;
if (state.src.charCodeAt(pos) === 93 /* ] */) {
break;
}
}
if (pos === start + 2) return false;
// no empty footnote labels
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 58 /* : */) return false;
if (silent) return true;
pos++;
if (!state.env.footnotes) state.env.footnotes = {};
if (!state.env.footnotes.refs) state.env.footnotes.refs = {};
const label = state.src.slice(start + 2, pos - 2);
state.env.footnotes.refs[`:${label}`] = -1;
const token_fref_o = new state.Token("footnote_reference_open", "", 1);
token_fref_o.meta = {
label: label
};
token_fref_o.level = state.level++;
state.tokens.push(token_fref_o);
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)) {
if (ch === 9) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
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 token_fref_c = new state.Token("footnote_reference_close", "", -1);
token_fref_c.level = --state.level;
state.tokens.push(token_fref_c);
return true;
}
// Process inline footnotes (^[...])
function footnote_inline(state, silent) {
const max = state.posMax;
const start = state.pos;
if (start + 2 >= max) return false;
if (state.src.charCodeAt(start) !== 94 /* ^ */) return false;
if (state.src.charCodeAt(start + 1) !== 91 /* [ */) 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) {
if (!state.env.footnotes) state.env.footnotes = {};
if (!state.env.footnotes.list) state.env.footnotes.list = [];
const footnoteId = state.env.footnotes.list.length;
const tokens = [];
state.md.inline.parse(state.src.slice(labelStart, labelEnd), state.md, state.env, tokens);
const token = state.push("footnote_ref", "", 0);
token.meta = {
id: footnoteId
};
state.env.footnotes.list[footnoteId] = {
content: state.src.slice(labelStart, labelEnd),
tokens: tokens
};
}
state.pos = labelEnd + 1;
state.posMax = max;
return true;
}
// Process footnote references ([^...])
function footnote_ref(state, silent) {
const max = state.posMax;
const start = state.pos;
// should be at least 4 chars - "[^x]"
if (start + 3 > max) return false;
if (!state.env.footnotes || !state.env.footnotes.refs) return false;
if (state.src.charCodeAt(start) !== 91 /* [ */) return false;
if (state.src.charCodeAt(start + 1) !== 94 /* ^ */) return false;
let pos;
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 32) return false;
if (state.src.charCodeAt(pos) === 10) return false;
if (state.src.charCodeAt(pos) === 93 /* ] */) {
break;
}
}
if (pos === start + 2) return false;
// no empty footnote labels
if (pos >= max) return false;
pos++;
const label = state.src.slice(start + 2, pos - 1);
if (typeof state.env.footnotes.refs[`:${label}`] === "undefined") return false;
if (!silent) {
if (!state.env.footnotes.list) state.env.footnotes.list = [];
let footnoteId;
if (state.env.footnotes.refs[`:${label}`] < 0) {
footnoteId = state.env.footnotes.list.length;
state.env.footnotes.list[footnoteId] = {
label: label,
count: 0
};
state.env.footnotes.refs[`:${label}`] = footnoteId;
} else {
footnoteId = state.env.footnotes.refs[`:${label}`];
}
const footnoteSubId = state.env.footnotes.list[footnoteId].count;
state.env.footnotes.list[footnoteId].count++;
const token = state.push("footnote_ref", "", 0);
token.meta = {
id: footnoteId,
subId: footnoteSubId,
label: label
};
}
state.pos = pos;
state.posMax = max;
return true;
}
// Glue footnote tokens to end of token stream
function footnote_tail(state) {
let tokens;
let current;
let currentLabel;
let insideRef = false;
const refTokens = {};
if (!state.env.footnotes) {
return;
}
state.tokens = state.tokens.filter((function(tok) {
if (tok.type === "footnote_reference_open") {
insideRef = true;
current = [];
currentLabel = tok.meta.label;
return false;
}
if (tok.type === "footnote_reference_close") {
insideRef = false;
// prepend ':' to avoid conflict with Object.prototype members
refTokens[":" + currentLabel] = current;
return false;
}
if (insideRef) {
current.push(tok);
}
return !insideRef;
}));
if (!state.env.footnotes.list) {
return;
}
const list = state.env.footnotes.list;
state.tokens.push(new state.Token("footnote_block_open", "", 1));
for (let i = 0, l = list.length; i < l; i++) {
const token_fo = new state.Token("footnote_open", "", 1);
token_fo.meta = {
id: i,
label: list[i].label
};
state.tokens.push(token_fo);
if (list[i].tokens) {
tokens = [];
const token_po = new state.Token("paragraph_open", "p", 1);
token_po.block = true;
tokens.push(token_po);
const token_i = new state.Token("inline", "", 0);
token_i.children = list[i].tokens;
token_i.content = list[i].content;
tokens.push(token_i);
const token_pc = new state.Token("paragraph_close", "p", -1);
token_pc.block = true;
tokens.push(token_pc);
} else if (list[i].label) {
tokens = refTokens[`:${list[i].label}`];
}
if (tokens) state.tokens = state.tokens.concat(tokens);
let lastParagraph;
if (state.tokens[state.tokens.length - 1].type === "paragraph_close") {
lastParagraph = state.tokens.pop();
} else {
lastParagraph = null;
}
const t = list[i].count > 0 ? list[i].count : 1;
for (let j = 0; j < t; j++) {
const token_a = new state.Token("footnote_anchor", "", 0);
token_a.meta = {
id: i,
subId: j,
label: list[i].label
};
state.tokens.push(token_a);
}
if (lastParagraph) {
state.tokens.push(lastParagraph);
}
state.tokens.push(new state.Token("footnote_close", "", -1));
}
state.tokens.push(new state.Token("footnote_block_close", "", -1));
}
md.block.ruler.before("reference", "footnote_def", footnote_def, {
alt: [ "paragraph", "reference" ]
});
md.inline.ruler.after("image", "footnote_inline", footnote_inline);
md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref);
md.core.ruler.after("inline", "footnote_tail", footnote_tail);
}
return footnote_plugin;
}));
File diff suppressed because one or more lines are too long
+353
View File
@@ -0,0 +1,353 @@
// Process footnotes
//
'use strict'
/// /////////////////////////////////////////////////////////////////////////////
// Renderer partials
function render_footnote_anchor_name (tokens, idx, options, env/*, slf */) {
const n = Number(tokens[idx].meta.id + 1).toString()
let prefix = ''
if (typeof env.docId === 'string') prefix = `-${env.docId}-`
return prefix + n
}
function render_footnote_caption (tokens, idx/*, options, env, slf */) {
let n = Number(tokens[idx].meta.id + 1).toString()
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`
return `[${n}]`
}
function render_footnote_ref (tokens, idx, options, env, slf) {
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf)
let refid = id
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`
}
function render_footnote_block_open (tokens, idx, options) {
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') +
'<section class="footnotes">\n' +
'<ol class="footnotes-list">\n'
}
function render_footnote_block_close () {
return '</ol>\n</section>\n'
}
function render_footnote_open (tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
return `<li id="fn${id}" class="footnote-item">`
}
function render_footnote_close () {
return '</li>\n'
}
function render_footnote_anchor (tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`
}
export default function footnote_plugin (md) {
const parseLinkLabel = md.helpers.parseLinkLabel
const isSpace = md.utils.isSpace
md.renderer.rules.footnote_ref = render_footnote_ref
md.renderer.rules.footnote_block_open = render_footnote_block_open
md.renderer.rules.footnote_block_close = render_footnote_block_close
md.renderer.rules.footnote_open = render_footnote_open
md.renderer.rules.footnote_close = render_footnote_close
md.renderer.rules.footnote_anchor = render_footnote_anchor
// helpers (only used in other rules, no tokens are attached to those)
md.renderer.rules.footnote_caption = render_footnote_caption
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name
// Process footnote block definition
function footnote_def (state, startLine, endLine, silent) {
const start = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// line should be at least 5 chars - "[^x]:"
if (start + 4 > max) return false
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false
if (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
}
}
if (pos === start + 2) return false // no empty footnote labels
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) return false
if (silent) return true
pos++
if (!state.env.footnotes) state.env.footnotes = {}
if (!state.env.footnotes.refs) state.env.footnotes.refs = {}
const label = state.src.slice(start + 2, pos - 2)
state.env.footnotes.refs[`:${label}`] = -1
const token_fref_o = new state.Token('footnote_reference_open', '', 1)
token_fref_o.meta = { label }
token_fref_o.level = state.level++
state.tokens.push(token_fref_o)
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)) {
if (ch === 0x09) {
offset += 4 - offset % 4
} else {
offset++
}
} else {
break
}
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 token_fref_c = new state.Token('footnote_reference_close', '', -1)
token_fref_c.level = --state.level
state.tokens.push(token_fref_c)
return true
}
// Process inline footnotes (^[...])
function footnote_inline (state, silent) {
const max = state.posMax
const start = state.pos
if (start + 2 >= max) return false
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) return false
if (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) {
if (!state.env.footnotes) state.env.footnotes = {}
if (!state.env.footnotes.list) state.env.footnotes.list = []
const footnoteId = state.env.footnotes.list.length
const tokens = []
state.md.inline.parse(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens
)
const token = state.push('footnote_ref', '', 0)
token.meta = { id: footnoteId }
state.env.footnotes.list[footnoteId] = {
content: state.src.slice(labelStart, labelEnd),
tokens
}
}
state.pos = labelEnd + 1
state.posMax = max
return true
}
// Process footnote references ([^...])
function footnote_ref (state, silent) {
const max = state.posMax
const start = state.pos
// should be at least 4 chars - "[^x]"
if (start + 3 > max) return false
if (!state.env.footnotes || !state.env.footnotes.refs) return false
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false
if (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) === 0x0A) return false
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
break
}
}
if (pos === start + 2) return false // no empty footnote labels
if (pos >= max) return false
pos++
const label = state.src.slice(start + 2, pos - 1)
if (typeof state.env.footnotes.refs[`:${label}`] === 'undefined') return false
if (!silent) {
if (!state.env.footnotes.list) state.env.footnotes.list = []
let footnoteId
if (state.env.footnotes.refs[`:${label}`] < 0) {
footnoteId = state.env.footnotes.list.length
state.env.footnotes.list[footnoteId] = { label, count: 0 }
state.env.footnotes.refs[`:${label}`] = footnoteId
} else {
footnoteId = state.env.footnotes.refs[`:${label}`]
}
const footnoteSubId = state.env.footnotes.list[footnoteId].count
state.env.footnotes.list[footnoteId].count++
const token = state.push('footnote_ref', '', 0)
token.meta = { id: footnoteId, subId: footnoteSubId, label }
}
state.pos = pos
state.posMax = max
return true
}
// Glue footnote tokens to end of token stream
function footnote_tail (state) {
let tokens
let current
let currentLabel
let insideRef = false
const refTokens = {}
if (!state.env.footnotes) { return }
state.tokens = state.tokens.filter(function (tok) {
if (tok.type === 'footnote_reference_open') {
insideRef = true
current = []
currentLabel = tok.meta.label
return false
}
if (tok.type === 'footnote_reference_close') {
insideRef = false
// prepend ':' to avoid conflict with Object.prototype members
refTokens[':' + currentLabel] = current
return false
}
if (insideRef) { current.push(tok) }
return !insideRef
})
if (!state.env.footnotes.list) { return }
const list = state.env.footnotes.list
state.tokens.push(new state.Token('footnote_block_open', '', 1))
for (let i = 0, l = list.length; i < l; i++) {
const token_fo = new state.Token('footnote_open', '', 1)
token_fo.meta = { id: i, label: list[i].label }
state.tokens.push(token_fo)
if (list[i].tokens) {
tokens = []
const token_po = new state.Token('paragraph_open', 'p', 1)
token_po.block = true
tokens.push(token_po)
const token_i = new state.Token('inline', '', 0)
token_i.children = list[i].tokens
token_i.content = list[i].content
tokens.push(token_i)
const token_pc = new state.Token('paragraph_close', 'p', -1)
token_pc.block = true
tokens.push(token_pc)
} else if (list[i].label) {
tokens = refTokens[`:${list[i].label}`]
}
if (tokens) state.tokens = state.tokens.concat(tokens)
let lastParagraph
if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
lastParagraph = state.tokens.pop()
} else {
lastParagraph = null
}
const t = list[i].count > 0 ? list[i].count : 1
for (let j = 0; j < t; j++) {
const token_a = new state.Token('footnote_anchor', '', 0)
token_a.meta = { id: i, subId: j, label: list[i].label }
state.tokens.push(token_a)
}
if (lastParagraph) {
state.tokens.push(lastParagraph)
}
state.tokens.push(new state.Token('footnote_close', '', -1))
}
state.tokens.push(new state.Token('footnote_block_close', '', -1))
}
md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: ['paragraph', 'reference'] })
md.inline.ruler.after('image', 'footnote_inline', footnote_inline)
md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref)
md.core.ruler.after('inline', 'footnote_tail', footnote_tail)
};
+48
View File
@@ -0,0 +1,48 @@
{
"name": "markdown-it-footnote",
"version": "4.0.0",
"description": "Footnotes for markdown-it markdown parser.",
"keywords": [
"markdown-it-plugin",
"markdown-it",
"markdown",
"footnotes"
],
"repository": "markdown-it/markdown-it-footnote",
"license": "MIT",
"main": "dist/index.cjs.js",
"module": "index.mjs",
"exports": {
".": {
"require": "./dist/index.cjs.js",
"import": "./index.mjs"
},
"./*": {
"require": "./*",
"import": "./*"
}
},
"files": [
"index.mjs",
"lib/",
"dist/"
],
"scripts": {
"lint": "eslint .",
"build": "rollup -c",
"test": "npm run lint && npm run build && c8 --exclude dist --exclude test -r text -r html -r lcov mocha",
"prepublishOnly": "npm run lint && npm run build"
},
"devDependencies": {
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"c8": "^8.0.1",
"eslint": "^8.55.0",
"eslint-config-standard": "^17.1.0",
"markdown-it": "^13.0.2",
"markdown-it-testgen": "^0.1.6",
"mocha": "^10.2.0",
"rollup": "^4.6.1"
}
}