diff --git a/lib/parser_block.js b/lib/parser_block.js
index 87001f6..3da8c49 100644
--- a/lib/parser_block.js
+++ b/lib/parser_block.js
@@ -14,6 +14,7 @@ var _rules = [
[ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'blockquote', 'list' ] ],
[ 'hr', require('./rules_block/hr'), [ 'paragraph', 'blockquote', 'list' ] ],
[ 'list', require('./rules_block/list'), [ 'paragraph', 'blockquote' ] ],
+ [ 'footnote', require('./rules_block/footnote'), [ 'paragraph' ] ],
[ 'heading', require('./rules_block/heading'), [ 'paragraph', 'blockquote' ] ],
[ 'lheading', require('./rules_block/lheading') ],
[ 'htmlblock', require('./rules_block/htmlblock'), [ 'paragraph', 'blockquote' ] ],
diff --git a/lib/parser_core.js b/lib/parser_core.js
index 512c926..fc44f3d 100644
--- a/lib/parser_core.js
+++ b/lib/parser_core.js
@@ -7,14 +7,15 @@ var Ruler = require('./ruler');
var _rules = [
- [ 'block', require('./rules_core/block') ],
- [ 'abbr', require('./rules_core/abbr') ],
- [ 'references', require('./rules_core/references') ],
- [ 'inline', require('./rules_core/inline') ],
- [ 'abbr2', require('./rules_core/abbr2') ],
- [ 'replacements', require('./rules_core/replacements') ],
- [ 'smartquotes', require('./rules_core/smartquotes') ],
- [ 'linkify', require('./rules_core/linkify') ]
+ [ 'block', require('./rules_core/block') ],
+ [ 'abbr', require('./rules_core/abbr') ],
+ [ 'references', require('./rules_core/references') ],
+ [ 'inline', require('./rules_core/inline') ],
+ [ 'footnote_block', require('./rules_core/footnote_block') ],
+ [ 'abbr2', require('./rules_core/abbr2') ],
+ [ 'replacements', require('./rules_core/replacements') ],
+ [ 'smartquotes', require('./rules_core/smartquotes') ],
+ [ 'linkify', require('./rules_core/linkify') ]
];
diff --git a/lib/parser_inline.js b/lib/parser_inline.js
index 755eafd..e80faa1 100644
--- a/lib/parser_inline.js
+++ b/lib/parser_inline.js
@@ -11,20 +11,22 @@ var replaceEntities = require('./common/utils').replaceEntities;
// Parser rules
var _rules = [
- [ 'text', require('./rules_inline/text') ],
- [ 'newline', require('./rules_inline/newline') ],
- [ 'escape', require('./rules_inline/escape') ],
- [ 'backticks', require('./rules_inline/backticks') ],
- [ 'del', require('./rules_inline/del') ],
- [ 'ins', require('./rules_inline/ins') ],
- [ 'mark', require('./rules_inline/mark') ],
- [ 'emphasis', require('./rules_inline/emphasis') ],
- [ 'sub', require('./rules_inline/sub') ],
- [ 'sup', require('./rules_inline/sup') ],
- [ 'links', require('./rules_inline/links') ],
- [ 'autolink', require('./rules_inline/autolink') ],
- [ 'htmltag', require('./rules_inline/htmltag') ],
- [ 'entity', require('./rules_inline/entity') ]
+ [ 'text', require('./rules_inline/text') ],
+ [ 'newline', require('./rules_inline/newline') ],
+ [ 'escape', require('./rules_inline/escape') ],
+ [ 'backticks', require('./rules_inline/backticks') ],
+ [ 'del', require('./rules_inline/del') ],
+ [ 'ins', require('./rules_inline/ins') ],
+ [ 'mark', require('./rules_inline/mark') ],
+ [ 'emphasis', require('./rules_inline/emphasis') ],
+ [ 'sub', require('./rules_inline/sub') ],
+ [ 'sup', require('./rules_inline/sup') ],
+ [ 'links', require('./rules_inline/links') ],
+ [ 'footnote_inline', require('./rules_inline/footnote_inline') ],
+ [ 'footnote_ref', require('./rules_inline/footnote_ref') ],
+ [ 'autolink', require('./rules_inline/autolink') ],
+ [ 'htmltag', require('./rules_inline/htmltag') ],
+ [ 'entity', require('./rules_inline/entity') ]
];
diff --git a/lib/renderer.js b/lib/renderer.js
index 4c1ff9a..05c693a 100644
--- a/lib/renderer.js
+++ b/lib/renderer.js
@@ -282,6 +282,29 @@ rules.abbr_close = function (/* tokens, idx, options, env */) {
};
+rules.footnote_ref = function (tokens, idx) {
+ var id = Number(tokens[idx].id + 1).toString();
+ return '';
+};
+rules.footnote_block_open = function (tokens, idx, options) {
+ return '
\n';
+};
+rules.footnote_open = function (tokens, idx) {
+ var id = Number(tokens[idx].id + 1).toString();
+ return '';
+};
+rules.footnote_close = function () {
+ return '\n';
+};
+rules.footnote_anchor = function (tokens, idx) {
+ var id = Number(tokens[idx].id + 1).toString();
+ return '↩';
+};
+
+
// Renderer class
function Renderer() {
// Clone rules object to allow local modifications
diff --git a/lib/rules_block/footnote.js b/lib/rules_block/footnote.js
new file mode 100644
index 0000000..5d8362f
--- /dev/null
+++ b/lib/rules_block/footnote.js
@@ -0,0 +1,67 @@
+// Process footnote reference list
+
+'use strict';
+
+
+module.exports = function footnote(state, startLine, endLine, silent) {
+ var oldBMark, oldTShift, oldParentType, pos, label,
+ 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; }
+ if (state.level >= state.options.maxNesting) { 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.available) { state.env.footnotes.available = Object.create(null); }
+ label = state.src.slice(start + 2, pos - 2);
+ state.env.footnotes.available[label] = true;
+
+ state.tokens.push({
+ type: 'footnote_reference_open',
+ label: label,
+ level: state.level++
+ });
+
+ oldBMark = state.bMarks[startLine];
+ oldTShift = state.tShift[startLine];
+ oldParentType = state.parentType;
+ state.tShift[startLine] = state.skipSpaces(pos) - pos;
+ state.bMarks[startLine] = pos;
+ state.blkIndent += 4;
+ state.parentType = 'footnote';
+
+ if (state.tShift[startLine] < state.blkIndent) {
+ state.tShift[startLine] += state.blkIndent;
+ state.bMarks[startLine] -= state.blkIndent;
+ }
+
+ state.parser.tokenize(state, startLine, endLine, true);
+
+ state.parentType = oldParentType;
+ state.blkIndent -= 4;
+ state.tShift[startLine] = oldTShift;
+ state.bMarks[startLine] = oldBMark;
+
+ state.tokens.push({
+ type: 'footnote_reference_close',
+ level: --state.level
+ });
+
+ return true;
+};
diff --git a/lib/rules_core/footnote_block.js b/lib/rules_core/footnote_block.js
new file mode 100644
index 0000000..4249474
--- /dev/null
+++ b/lib/rules_core/footnote_block.js
@@ -0,0 +1,88 @@
+'use strict';
+
+
+module.exports = function footnote_block(state) {
+ var i, l, t, list, tokens, current, currentLabel, anchor,
+ level = 0,
+ insideRef = false,
+ refTokens = Object.create(null);
+
+ if (!state.env.footnotes) { return; }
+
+ state.tokens = state.tokens.filter(function(tok) {
+ if (tok.type === 'footnote_reference_open') {
+ insideRef = true;
+ current = [];
+ currentLabel = tok.label;
+ return false;
+ }
+ if (tok.type === 'footnote_reference_close') {
+ insideRef = false;
+ refTokens[currentLabel] = current;
+ return false;
+ }
+ if (insideRef) { current.push(tok); }
+ return !insideRef;
+ });
+
+ if (!state.env.footnotes.list) { return; }
+ list = state.env.footnotes.list;
+
+ state.tokens.push({
+ type: 'footnote_block_open',
+ level: level++
+ });
+ for (i = 0, l = list.length; i < l; i++) {
+ state.tokens.push({
+ type: 'footnote_open',
+ id: i,
+ level: level++
+ });
+
+ if (list[i].tokens) {
+ tokens = [];
+ tokens.push({
+ type: 'paragraph_open',
+ tight: false,
+ level: level++
+ });
+ tokens.push({
+ type: 'inline',
+ content: '',
+ level: level,
+ children: list[i].tokens
+ });
+ tokens.push({
+ type: 'paragraph_close',
+ tight: false,
+ level: --level
+ });
+ } else if (list[i].label) {
+ tokens = refTokens[list[i].label];
+ }
+
+ anchor = {
+ type: 'footnote_anchor',
+ id: i,
+ level: level
+ };
+
+ state.tokens = state.tokens.concat(tokens);
+ if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
+ t = state.tokens.pop();
+ state.tokens.push(anchor);
+ state.tokens.push(t);
+ } else {
+ state.tokens.push(anchor);
+ }
+
+ state.tokens.push({
+ type: 'footnote_close',
+ level: --level
+ });
+ }
+ state.tokens.push({
+ type: 'footnote_block_close',
+ level: --level
+ });
+};
diff --git a/lib/rules_inline/footnote_inline.js b/lib/rules_inline/footnote_inline.js
new file mode 100644
index 0000000..e5dc90a
--- /dev/null
+++ b/lib/rules_inline/footnote_inline.js
@@ -0,0 +1,54 @@
+// Process inline footnotes (^[...])
+
+'use strict';
+
+var parseLinkLabel = require('../helpers/parse_link_label');
+
+
+module.exports = function footnote_inline(state, silent) {
+ var labelStart,
+ labelEnd,
+ pos,
+ footnoteId,
+ oldLength,
+ 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; }
+ if (state.level >= state.options.maxNesting) { 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.pos = labelStart;
+ state.posMax = labelEnd;
+
+ state.push({
+ type: 'footnote_ref',
+ id: footnoteId,
+ level: state.level
+ });
+ state.linkLevel++;
+ oldLength = state.tokens.length;
+ state.parser.tokenize(state);
+ state.env.footnotes.list[footnoteId] = { tokens: state.tokens.splice(oldLength) };
+ state.linkLevel--;
+ }
+
+ state.pos = pos;
+ state.posMax = max;
+ return true;
+};
diff --git a/lib/rules_inline/footnote_ref.js b/lib/rules_inline/footnote_ref.js
new file mode 100644
index 0000000..6fce842
--- /dev/null
+++ b/lib/rules_inline/footnote_ref.js
@@ -0,0 +1,52 @@
+// Process footnote references ([^...])
+
+'use strict';
+
+
+module.exports = function footnote_ref(state, silent) {
+ var label,
+ pos,
+ footnoteId,
+ 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.available) { return false; }
+ if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
+ if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
+ if (state.level >= state.options.maxNesting) { 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 (!state.env.footnotes.available[label]) { return false; }
+
+ if (!silent) {
+ if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
+ footnoteId = state.env.footnotes.list.length;
+
+ state.push({
+ type: 'footnote_ref',
+ id: footnoteId,
+ level: state.level
+ });
+
+ state.env.footnotes.list[footnoteId] = { label: label };
+ }
+
+ state.pos = pos;
+ state.posMax = max;
+ return true;
+};
diff --git a/test/fixtures/remarkable/footnotes.txt b/test/fixtures/remarkable/footnotes.txt
new file mode 100644
index 0000000..0a6ebaa
--- /dev/null
+++ b/test/fixtures/remarkable/footnotes.txt
@@ -0,0 +1,179 @@
+
+Pandoc example:
+.
+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.
+
+ { some.code }
+
+ The whole paragraph can be indented, or just the first
+ line. In this way, multi-paragraph footnotes work like
+ multi-paragraph list items.
+
+This paragraph won't be part of the note, because it
+isn't indented.
+.
+Here is a footnote reference, and another.
+This paragraph won’t be part of the note, because it
+isn’t indented.
+
+.
+
+They could terminate each other:
+
+.
+[^1][^2][^3]
+
+[^1]: foo
+[^2]: bar
+[^3]: baz
+.
+
+
+.
+
+
+They could be inside blockquotes, and are lazy:
+.
+[^foo]
+
+> [^foo]: bar
+baz
+.
+
+
+
+
+.
+
+
+Their labels could not contain spaces or newlines:
+
+.
+[^ foo]: bar baz
+
+[^foo
+]: bar baz
+.
+[^ foo]: bar baz
+[^foo
+]: bar baz
+.
+
+We support inline notes too (pandoc example):
+
+.
+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.]
+.
+Here is an inline note.
+
+.
+
+They could have arbitrary markup:
+
+.
+foo^[ *bar* ]
+.
+foo
+
+.
+
+Indents:
+
+.
+[^xxxxx] [^yyyyy]
+
+[^xxxxx]: foo
+ ---
+
+[^yyyyy]: foo
+ ---
+.
+
+
+
+.
+
+Indents for the first line:
+
+.
+[^xxxxx] [^yyyyy]
+
+[^xxxxx]: foo
+
+[^yyyyy]: foo
+.
+
+
+.