Browse Source

Add footnote rules

pull/14/head
Alex Kocharin 10 years ago
parent
commit
faae7485b7
  1. 1
      lib/parser_block.js
  2. 1
      lib/parser_core.js
  3. 2
      lib/parser_inline.js
  4. 23
      lib/renderer.js
  5. 67
      lib/rules_block/footnote.js
  6. 88
      lib/rules_core/footnote_block.js
  7. 54
      lib/rules_inline/footnote_inline.js
  8. 52
      lib/rules_inline/footnote_ref.js
  9. 179
      test/fixtures/remarkable/footnotes.txt

1
lib/parser_block.js

@ -14,6 +14,7 @@ var _rules = [
[ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'blockquote', 'list' ] ], [ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'blockquote', 'list' ] ],
[ 'hr', require('./rules_block/hr'), [ 'paragraph', 'blockquote', 'list' ] ], [ 'hr', require('./rules_block/hr'), [ 'paragraph', 'blockquote', 'list' ] ],
[ 'list', require('./rules_block/list'), [ 'paragraph', 'blockquote' ] ], [ 'list', require('./rules_block/list'), [ 'paragraph', 'blockquote' ] ],
[ 'footnote', require('./rules_block/footnote'), [ 'paragraph' ] ],
[ 'heading', require('./rules_block/heading'), [ 'paragraph', 'blockquote' ] ], [ 'heading', require('./rules_block/heading'), [ 'paragraph', 'blockquote' ] ],
[ 'lheading', require('./rules_block/lheading') ], [ 'lheading', require('./rules_block/lheading') ],
[ 'htmlblock', require('./rules_block/htmlblock'), [ 'paragraph', 'blockquote' ] ], [ 'htmlblock', require('./rules_block/htmlblock'), [ 'paragraph', 'blockquote' ] ],

1
lib/parser_core.js

@ -11,6 +11,7 @@ var _rules = [
[ 'abbr', require('./rules_core/abbr') ], [ 'abbr', require('./rules_core/abbr') ],
[ 'references', require('./rules_core/references') ], [ 'references', require('./rules_core/references') ],
[ 'inline', require('./rules_core/inline') ], [ 'inline', require('./rules_core/inline') ],
[ 'footnote_block', require('./rules_core/footnote_block') ],
[ 'abbr2', require('./rules_core/abbr2') ], [ 'abbr2', require('./rules_core/abbr2') ],
[ 'replacements', require('./rules_core/replacements') ], [ 'replacements', require('./rules_core/replacements') ],
[ 'smartquotes', require('./rules_core/smartquotes') ], [ 'smartquotes', require('./rules_core/smartquotes') ],

2
lib/parser_inline.js

@ -22,6 +22,8 @@ var _rules = [
[ 'sub', require('./rules_inline/sub') ], [ 'sub', require('./rules_inline/sub') ],
[ 'sup', require('./rules_inline/sup') ], [ 'sup', require('./rules_inline/sup') ],
[ 'links', require('./rules_inline/links') ], [ 'links', require('./rules_inline/links') ],
[ 'footnote_inline', require('./rules_inline/footnote_inline') ],
[ 'footnote_ref', require('./rules_inline/footnote_ref') ],
[ 'autolink', require('./rules_inline/autolink') ], [ 'autolink', require('./rules_inline/autolink') ],
[ 'htmltag', require('./rules_inline/htmltag') ], [ 'htmltag', require('./rules_inline/htmltag') ],
[ 'entity', require('./rules_inline/entity') ] [ 'entity', require('./rules_inline/entity') ]

23
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 '<a href="#fn' + id + '" class="footnoteRef" id="fnref' + id + '"><sup>' + id + '</sup></a>';
};
rules.footnote_block_open = function (tokens, idx, options) {
return '<div class="footnotes">\n' + (options.xhtmlOut ? '<hr />' : '<hr>') + '\n<ol>\n';
};
rules.footnote_block_close = function () {
return '</ol>\n</div>\n';
};
rules.footnote_open = function (tokens, idx) {
var id = Number(tokens[idx].id + 1).toString();
return '<li id="fn' + id + '">';
};
rules.footnote_close = function () {
return '</li>\n';
};
rules.footnote_anchor = function (tokens, idx) {
var id = Number(tokens[idx].id + 1).toString();
return '<a href="#fnref' + id + '">↩</a>';
};
// Renderer class // Renderer class
function Renderer() { function Renderer() {
// Clone rules object to allow local modifications // Clone rules object to allow local modifications

67
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;
};

88
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
});
};

54
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;
};

52
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;
};

179
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.
.
<p>Here is a footnote reference,<a href="#fn1" class="footnoteRef" id="fnref1"><sup>1</sup></a> and another.<a href="#fn2" class="footnoteRef" id="fnref2"><sup>2</sup></a></p>
<p>This paragraph won’t be part of the note, because it
isn’t indented.</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn1"><p>Here is the footnote.<a href="#fnref1">↩</a></p>
</li>
<li id="fn2"><p>Here’s one with multiple blocks.</p>
<p>Subsequent paragraphs are indented to show that they
belong to the previous footnote.</p>
<pre><code>{ some.code }
</code></pre>
<p>The whole paragraph can be indented, or just the first
line. In this way, multi-paragraph footnotes work like
multi-paragraph list items.<a href="#fnref2">↩</a></p>
</li>
</ol>
</div>
.
They could terminate each other:
.
[^1][^2][^3]
[^1]: foo
[^2]: bar
[^3]: baz
.
<p><a href="#fn1" class="footnoteRef" id="fnref1"><sup>1</sup></a><a href="#fn2" class="footnoteRef" id="fnref2"><sup>2</sup></a><a href="#fn3" class="footnoteRef" id="fnref3"><sup>3</sup></a></p>
<div class="footnotes">
<hr>
<ol>
<li id="fn1"><p>foo<a href="#fnref1">↩</a></p>
</li>
<li id="fn2"><p>bar<a href="#fnref2">↩</a></p>
</li>
<li id="fn3"><p>baz<a href="#fnref3">↩</a></p>
</li>
</ol>
</div>
.
They could be inside blockquotes, and are lazy:
.
[^foo]
> [^foo]: bar
baz
.
<p><a href="#fn1" class="footnoteRef" id="fnref1"><sup>1</sup></a></p>
<blockquote>
</blockquote>
<div class="footnotes">
<hr>
<ol>
<li id="fn1"><p>bar
baz<a href="#fnref1">↩</a></p>
</li>
</ol>
</div>
.
Their labels could not contain spaces or newlines:
.
[^ foo]: bar baz
[^foo
]: bar baz
.
<p>[^ foo]: bar baz</p>
<p>[^foo
]: bar baz</p>
.
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.]
.
<p>Here is an inline note.<a href="#fn1" class="footnoteRef" id="fnref1"><sup>1</sup></a></p>
<div class="footnotes">
<hr>
<ol>
<li id="fn1"><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">↩</a></p>
</li>
</ol>
</div>
.
They could have arbitrary markup:
.
foo^[ *bar* ]
.
<p>foo<a href="#fn1" class="footnoteRef" id="fnref1"><sup>1</sup></a></p>
<div class="footnotes">
<hr>
<ol>
<li id="fn1"><p> <em>bar</em> <a href="#fnref1">↩</a></p>
</li>
</ol>
</div>
.
Indents:
.
[^xxxxx] [^yyyyy]
[^xxxxx]: foo
---
[^yyyyy]: foo
---
.
<p><a href="#fn1" class="footnoteRef" id="fnref1"><sup>1</sup></a> <a href="#fn2" class="footnoteRef" id="fnref2"><sup>2</sup></a></p>
<hr>
<div class="footnotes">
<hr>
<ol>
<li id="fn1"><h2>foo</h2>
<a href="#fnref1">↩</a></li>
<li id="fn2"><p>foo<a href="#fnref2">↩</a></p>
</li>
</ol>
</div>
.
Indents for the first line:
.
[^xxxxx] [^yyyyy]
[^xxxxx]: foo
[^yyyyy]: foo
.
<p><a href="#fn1" class="footnoteRef" id="fnref1"><sup>1</sup></a> <a href="#fn2" class="footnoteRef" id="fnref2"><sup>2</sup></a></p>
<div class="footnotes">
<hr>
<ol>
<li id="fn1"><p>foo<a href="#fnref1">↩</a></p>
</li>
<li id="fn2"><pre><code>foo
</code></pre>
<a href="#fnref2">↩</a></li>
</ol>
</div>
.
Loading…
Cancel
Save