You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
327 lines
10 KiB
327 lines
10 KiB
1 month ago
|
// Process footnotes
|
||
|
//
|
||
|
'use strict';
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////
|
||
|
// Renderer partials
|
||
|
|
||
|
function _footnote_ref(tokens, idx) {
|
||
|
var n = Number(tokens[idx].meta.id + 1).toString();
|
||
|
var id = 'fnref' + n;
|
||
|
if (tokens[idx].meta.subId > 0) {
|
||
|
id += ':' + tokens[idx].meta.subId;
|
||
|
}
|
||
|
return '<sup class="footnote-ref"><a href="#fn' + n + '" id="' + id + '">[' + n + ']</a></sup>';
|
||
|
}
|
||
|
function _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 _footnote_block_close() {
|
||
|
return '</ol>\n</section>\n';
|
||
|
}
|
||
|
function _footnote_open(tokens, idx) {
|
||
|
var id = Number(tokens[idx].meta.id + 1).toString();
|
||
|
return '<li id="fn' + id + '" class="footnote-item">';
|
||
|
}
|
||
|
function _footnote_close() {
|
||
|
return '</li>\n';
|
||
|
}
|
||
|
function _footnote_anchor(tokens, idx) {
|
||
|
var n = Number(tokens[idx].meta.id + 1).toString();
|
||
|
var id = 'fnref' + n;
|
||
|
if (tokens[idx].meta.subId > 0) {
|
||
|
id += ':' + tokens[idx].meta.subId;
|
||
|
}
|
||
|
return ' <a href="#' + id + '" class="footnote-backref">\u21a9</a>'; /* ↩ */
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
|
||
|
module.exports = function sub_plugin(md) {
|
||
|
var parseLinkLabel = md.helpers.parseLinkLabel,
|
||
|
isSpace = md.utils.isSpace;
|
||
|
|
||
|
md.renderer.rules.footnote_ref = _footnote_ref;
|
||
|
md.renderer.rules.footnote_block_open = _footnote_block_open;
|
||
|
md.renderer.rules.footnote_block_close = _footnote_block_close;
|
||
|
md.renderer.rules.footnote_open = _footnote_open;
|
||
|
md.renderer.rules.footnote_close = _footnote_close;
|
||
|
md.renderer.rules.footnote_anchor = _footnote_anchor;
|
||
|
|
||
|
// Process footnote block definition
|
||
|
function footnote_def(state, startLine, endLine, silent) {
|
||
|
var oldBMark, oldTShift, oldSCount, oldParentType, pos, label, token,
|
||
|
initial, offset, ch, posAfterColon,
|
||
|
start = state.bMarks[startLine] + state.tShift[startLine],
|
||
|
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; }
|
||
|
|
||
|
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 = {}; }
|
||
|
label = state.src.slice(start + 2, pos - 2);
|
||
|
state.env.footnotes.refs[':' + label] = -1;
|
||
|
|
||
|
token = new state.Token('footnote_reference_open', '', 1);
|
||
|
token.meta = { label: label };
|
||
|
token.level = state.level++;
|
||
|
state.tokens.push(token);
|
||
|
|
||
|
oldBMark = state.bMarks[startLine];
|
||
|
oldTShift = state.tShift[startLine];
|
||
|
oldSCount = state.sCount[startLine];
|
||
|
oldParentType = state.parentType;
|
||
|
|
||
|
posAfterColon = pos;
|
||
|
initial = offset = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
|
||
|
|
||
|
while (pos < max) {
|
||
|
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;
|
||
|
|
||
|
token = new state.Token('footnote_reference_close', '', -1);
|
||
|
token.level = --state.level;
|
||
|
state.tokens.push(token);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Process inline footnotes (^[...])
|
||
|
function footnote_inline(state, silent) {
|
||
|
var labelStart,
|
||
|
labelEnd,
|
||
|
footnoteId,
|
||
|
token,
|
||
|
tokens,
|
||
|
max = state.posMax,
|
||
|
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; }
|
||
|
|
||
|
labelStart = start + 2;
|
||
|
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 = []; }
|
||
|
footnoteId = state.env.footnotes.list.length;
|
||
|
|
||
|
state.md.inline.parse(
|
||
|
state.src.slice(labelStart, labelEnd),
|
||
|
state.md,
|
||
|
state.env,
|
||
|
tokens = []
|
||
|
);
|
||
|
|
||
|
token = state.push('footnote_ref', '', 0);
|
||
|
token.meta = { id: footnoteId };
|
||
|
|
||
|
state.env.footnotes.list[footnoteId] = { tokens: tokens };
|
||
|
}
|
||
|
|
||
|
state.pos = labelEnd + 1;
|
||
|
state.posMax = max;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Process footnote references ([^...])
|
||
|
function footnote_ref(state, silent) {
|
||
|
var label,
|
||
|
pos,
|
||
|
footnoteId,
|
||
|
footnoteSubId,
|
||
|
token,
|
||
|
max = state.posMax,
|
||
|
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; }
|
||
|
|
||
|
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++;
|
||
|
|
||
|
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 = []; }
|
||
|
|
||
|
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];
|
||
|
}
|
||
|
|
||
|
footnoteSubId = state.env.footnotes.list[footnoteId].count;
|
||
|
state.env.footnotes.list[footnoteId].count++;
|
||
|
|
||
|
token = state.push('footnote_ref', '', 0);
|
||
|
token.meta = { id: footnoteId, subId: footnoteSubId };
|
||
|
}
|
||
|
|
||
|
state.pos = pos;
|
||
|
state.posMax = max;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Glue footnote tokens to end of token stream
|
||
|
function footnote_tail(state) {
|
||
|
var i, l, j, t, lastParagraph, list, token, tokens, current, currentLabel,
|
||
|
insideRef = false,
|
||
|
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; }
|
||
|
list = state.env.footnotes.list;
|
||
|
|
||
|
token = new state.Token('footnote_block_open', '', 1);
|
||
|
state.tokens.push(token);
|
||
|
|
||
|
for (i = 0, l = list.length; i < l; i++) {
|
||
|
token = new state.Token('footnote_open', '', 1);
|
||
|
token.meta = { id: i };
|
||
|
state.tokens.push(token);
|
||
|
|
||
|
if (list[i].tokens) {
|
||
|
tokens = [];
|
||
|
|
||
|
token = new state.Token('paragraph_open', 'p', 1);
|
||
|
token.block = true;
|
||
|
tokens.push(token);
|
||
|
|
||
|
token = new state.Token('inline', '', 0);
|
||
|
token.children = list[i].tokens;
|
||
|
token.content = '';
|
||
|
tokens.push(token);
|
||
|
|
||
|
token = new state.Token('paragraph_close', 'p', -1);
|
||
|
token.block = true;
|
||
|
tokens.push(token);
|
||
|
|
||
|
} else if (list[i].label) {
|
||
|
tokens = refTokens[':' + list[i].label];
|
||
|
}
|
||
|
|
||
|
state.tokens = state.tokens.concat(tokens);
|
||
|
if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
|
||
|
lastParagraph = state.tokens.pop();
|
||
|
} else {
|
||
|
lastParagraph = null;
|
||
|
}
|
||
|
|
||
|
t = list[i].count > 0 ? list[i].count : 1;
|
||
|
for (j = 0; j < t; j++) {
|
||
|
token = new state.Token('footnote_anchor', '', 0);
|
||
|
token.meta = { id: i, subId: j };
|
||
|
state.tokens.push(token);
|
||
|
}
|
||
|
|
||
|
if (lastParagraph) {
|
||
|
state.tokens.push(lastParagraph);
|
||
|
}
|
||
|
|
||
|
token = new state.Token('footnote_close', '', -1);
|
||
|
state.tokens.push(token);
|
||
|
}
|
||
|
|
||
|
token = new state.Token('footnote_block_close', '', -1);
|
||
|
state.tokens.push(token);
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
};
|