From faae7485b791150895cb51eedd1c5996ace760c1 Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Sat, 15 Nov 2014 04:20:00 +0300 Subject: [PATCH] Add footnote rules --- lib/parser_block.js | 1 + lib/parser_core.js | 17 +-- lib/parser_inline.js | 30 +++-- lib/renderer.js | 23 ++++ lib/rules_block/footnote.js | 67 +++++++++ lib/rules_core/footnote_block.js | 88 ++++++++++++ lib/rules_inline/footnote_inline.js | 54 ++++++++ lib/rules_inline/footnote_ref.js | 52 +++++++ test/fixtures/remarkable/footnotes.txt | 179 +++++++++++++++++++++++++ 9 files changed, 489 insertions(+), 22 deletions(-) create mode 100644 lib/rules_block/footnote.js create mode 100644 lib/rules_core/footnote_block.js create mode 100644 lib/rules_inline/footnote_inline.js create mode 100644 lib/rules_inline/footnote_ref.js create mode 100644 test/fixtures/remarkable/footnotes.txt 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 '' + id + ''; +}; +rules.footnote_block_open = function (tokens, idx, options) { + return '
\n' + (options.xhtmlOut ? '
' : '
') + '\n
    \n'; +}; +rules.footnote_block_close = function () { + return '
\n
\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,1 and another.2

    +

    This paragraph won’t be part of the note, because it +isn’t indented.

    +
    +
    +
      +
    1. Here is the footnote.

      +
    2. +
    3. 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.

      +
    4. +
    +
    +. + +They could terminate each other: + +. +[^1][^2][^3] + +[^1]: foo +[^2]: bar +[^3]: baz +. +

    123

    +
    +
    +
      +
    1. foo

      +
    2. +
    3. bar

      +
    4. +
    5. baz

      +
    6. +
    +
    +. + + +They could be inside blockquotes, and are lazy: +. +[^foo] + +> [^foo]: bar +baz +. +

    1

    +
    +
    +
    +
    +
      +
    1. bar +baz

      +
    2. +
    +
    +. + + +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.1

    +
    +
    +
      +
    1. Inlines notes are easier to write, since +you don’t have to pick an identifier and move down to type the +note.

      +
    2. +
    +
    +. + +They could have arbitrary markup: + +. +foo^[ *bar* ] +. +

    foo1

    +
    +
    +
      +
    1. bar

      +
    2. +
    +
    +. + +Indents: + +. +[^xxxxx] [^yyyyy] + +[^xxxxx]: foo + --- + +[^yyyyy]: foo + --- +. +

    1 2

    +
    +
    +
    +
      +
    1. foo

      +
    2. +
    3. foo

      +
    4. +
    +
    +. + +Indents for the first line: + +. +[^xxxxx] [^yyyyy] + +[^xxxxx]: foo + +[^yyyyy]: foo +. +

    1 2

    +
    +
    +
      +
    1. foo

      +
    2. +
    3. foo
      +
      +
    4. +
    +
    +.