diff --git a/lib/index.js b/lib/index.js index dce8cb3..acb8bf7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,8 +9,7 @@ var Renderer = require('./renderer'); var ParserBlock = require('./parser_block'); var ParserInline = require('./parser_inline'); var Typographer = require('./typographer'); -var Linkifier = require('./linkifier'); - +var Ruler = require('./ruler'); var config = { 'default': require('./configs/default'), @@ -18,6 +17,26 @@ var config = { commonmark: require('./configs/commonmark') }; +var _rules = [ + [ 'block', require('./rules_core/block') ], + [ 'references', require('./rules_core/references') ], + [ 'inline', require('./rules_core/inline') ], + [ 'linkify', require('./rules_core/linkify') ], + [ 'typographer', require('./rules_core/typographer') ] +]; + + +function StateCore(self, src, env) { + this.src = src; + this.env = env; + this.options = self.options; + this.tokens = []; + + this.inline = self.inline; + this.block = self.block; + this.renderer = self.renderer; + this.typographer = self.typographer; +} // Main class // @@ -29,23 +48,19 @@ function Remarkable(presetName, options) { } } - this.options = {}; - this.state = null; - this.inline = new ParserInline(); this.block = new ParserBlock(); this.renderer = new Renderer(); + this.ruler = new Ruler(); this.typographer = new Typographer(); - this.linkifier = new Linkifier(); - - // Cross-references to simplify code (a bit dirty, but easy). - this.block.inline = this.inline; - this.inline.typographer = this.typographer; - this.inline.linkifier = this.linkifier; + this.options = {}; this.configure(config[presetName]); - if (options) { this.set(options); } + + for (var i = 0; i < _rules.length; i++) { + this.ruler.push(_rules[i][0], _rules[i][1]); + } } @@ -96,19 +111,17 @@ Remarkable.prototype.use = function (plugin, opts) { // definitions data. // Remarkable.prototype.parse = function (src, env) { - var tokens, tok, i, l; - // Parse blocks - tokens = this.block.parse(src, this.options, env); - - // Parse inlines - for (i = 0, l = tokens.length; i < l; i++) { - tok = tokens[i]; - if (tok.type === 'inline') { - tok.children = this.inline.parse(tok.content, this.options, env); - } + var i, len, + rules = this.ruler.getRules(''), + state = new StateCore(this, src, env); + + len = rules.length; + + for (i = 0; i < len; i++) { + rules[i](state); } - return tokens; + return state.tokens; }; // Main method that does all magic :) diff --git a/lib/linkifier.js b/lib/linkifier.js deleted file mode 100644 index e28c196..0000000 --- a/lib/linkifier.js +++ /dev/null @@ -1,42 +0,0 @@ -// Class of link replacement rules -// -'use strict'; - - -var assign = require('./common/utils').assign; -var Ruler = require('./ruler'); - - -var _rules = [ - [ 'linkify', require('./rules_text/linkify') ] -]; - - -function Linkifier() { - this.options = {}; - - this.ruler = new Ruler(); - - for (var i = 0; i < _rules.length; i++) { - this.ruler.push(_rules[i][0], _rules[i][1]); - } -} - - -Linkifier.prototype.set = function (options) { - assign(this.options, options); -}; - - -Linkifier.prototype.process = function (state) { - var i, l, rules; - - rules = this.ruler.getRules(''); - - for (i = 0, l = rules.length; i < l; i++) { - rules[i](this, state); - } -}; - - -module.exports = Linkifier; diff --git a/lib/parser_inline.js b/lib/parser_inline.js index 5384d85..1e73998 100644 --- a/lib/parser_inline.js +++ b/lib/parser_inline.js @@ -127,16 +127,7 @@ ParserInline.prototype.tokenize = function (state) { ParserInline.prototype.parse = function (str, options, env) { var state = new StateInline(str, this, options, env); - this.tokenize(state); - - if (options.linkify) { - this.linkifier.process(state); - } - if (options.typographer) { - this.typographer.process(state); - } - - return state.tokens; + return this.tokenize(state); }; diff --git a/lib/renderer.js b/lib/renderer.js index 35a49e7..89d104f 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -42,10 +42,23 @@ function escapeHtml(str) { return str; } +function nextToken(tokens, idx) { + if (++idx >= tokens.length - 2) { return idx; } + if (tokens[idx].type === 'paragraph_open' && tokens[idx].tight) { + if (tokens[idx + 1].type === 'inline' && tokens[idx + 1].content.length === 0) { + if (tokens[idx + 2].type === 'paragraph_close' && tokens[idx + 2].tight) { + return nextToken(tokens, idx + 2); + } + } + } + return idx; +} + // check if we need to hide '\n' before next token function getBreak(tokens, idx) { - if (++idx < tokens.length && + idx = nextToken(tokens, idx); + if (idx < tokens.length && tokens[idx].type === 'list_item_close') { return ''; } @@ -136,7 +149,8 @@ rules.paragraph_open = function (tokens, idx/*, options*/) { return tokens[idx].tight ? '' : '
'; }; rules.paragraph_close = function (tokens, idx /*, options*/) { - return (tokens[idx].tight ? '' : '
') + getBreak(tokens, idx); + var addBreak = !(tokens[idx].tight && idx && tokens[idx - 1].type === 'inline' && !tokens[idx - 1].content); + return (tokens[idx].tight ? '' : '') + (addBreak ? getBreak(tokens, idx) : ''); }; diff --git a/lib/rules_block/paragraph.js b/lib/rules_block/paragraph.js index 2ebac81..d25e5da 100644 --- a/lib/rules_block/paragraph.js +++ b/lib/rules_block/paragraph.js @@ -3,11 +3,8 @@ 'use strict'; -var parseRef = require('../parser_ref'); - - module.exports = function paragraph(state, startLine/*, endLine*/) { - var endLine, content, pos, terminate, i, l, + var endLine, content, terminate, i, l, nextLine = startLine + 1, terminatorRules; @@ -36,12 +33,6 @@ module.exports = function paragraph(state, startLine/*, endLine*/) { content = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); - while (content.length) { - pos = parseRef(content, state.parser.inline, state.options, state.env); - if (pos < 0) { break; } - content = content.slice(pos).trim(); - } - state.line = nextLine; if (content.length) { state.tokens.push({ diff --git a/lib/rules_core/block.js b/lib/rules_core/block.js new file mode 100644 index 0000000..01769bc --- /dev/null +++ b/lib/rules_core/block.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = function block(state) { + var tokens = state.block.parse(state.src, state.options, state.env); + state.tokens = state.tokens.concat(tokens); +}; diff --git a/lib/rules_core/inline.js b/lib/rules_core/inline.js new file mode 100644 index 0000000..a019de9 --- /dev/null +++ b/lib/rules_core/inline.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = function inline(state) { + var tokens = state.tokens, tok, i, l; + + // Parse inlines + for (i = 0, l = tokens.length; i < l; i++) { + tok = tokens[i]; + if (tok.type === 'inline') { + tok.children = state.inline.parse(tok.content, state.options, state.env); + } + } +}; diff --git a/lib/rules_core/linkify.js b/lib/rules_core/linkify.js new file mode 100644 index 0000000..acb2f36 --- /dev/null +++ b/lib/rules_core/linkify.js @@ -0,0 +1,128 @@ +// Replace link-like texts with link nodes. +// +// Currently restricted to http/https/ftp +// +'use strict'; + + +var Autolinker = require('autolinker'); + + +var LINK_SCAN_RE = /www|\:\/\//; + +var links = []; +var autolinker = new Autolinker({ + stripPrefix: false, + replaceFn: function (autolinker, match) { + // Only collect matched strings but don't change anything. + if (match.getType() === 'url') { + links.push({ text: match.matchedText, url: match.getUrl() }); + } + return false; + } +}); + +function isLinkOpen(str) { + return /^\s]/i.test(str); +} +function isLinkClose(str) { + return /^<\/a\s*>/i.test(str); +} + + +module.exports = function linkify(state) { + var i, j, l, tokens, token, text, nodes, ln, pos, level, htmlLinkLevel, + blockTokens = state.tokens; + + if (!state.options.linkify) { return; } + + for (j = 0, l = blockTokens.length; j < l; j++) { + if (blockTokens[j].type !== 'inline') { continue; } + tokens = blockTokens[j].children; + + htmlLinkLevel = 0; + + // We scan from the end, to keep position when new tags added. + // Use reversed logic in links start/end match + for (i = tokens.length - 1; i >= 0; i--) { + token = tokens[i]; + + // Skip content of markdown links + if (token.type === 'link_close') { + i--; + while (tokens[i].level !== token.level && tokens[i].type !== 'link_open') { + i--; + } + continue; + } + + // Skip content of html tag links + if (token.type === 'htmltag') { + if (isLinkOpen(token.content) && htmlLinkLevel > 0) { + htmlLinkLevel--; + } + if (isLinkClose(token.content)) { + htmlLinkLevel++; + } + } + if (htmlLinkLevel > 0) { continue; } + + if (token.type === 'text' && LINK_SCAN_RE.test(token.content)) { + + text = token.content; + links.length = 0; + autolinker.link(text); + + if (!links.length) { continue; } + + // Now split string to nodes + nodes = []; + level = token.level; + + for (ln = 0; ln < links.length; ln++) { + + if (!state.inline.validateLink(links[ln].url)) { continue; } + + pos = text.indexOf(links[ln].text); + + if (pos === -1) { continue; } + + if (pos) { + level = level; + nodes.push({ + type: 'text', + content: text.slice(0, pos), + level: level + }); + } + nodes.push({ + type: 'link_open', + href: links[ln].url, + title: '', + level: level++ + }); + nodes.push({ + type: 'text', + content: links[ln].text, + level: level + }); + nodes.push({ + type: 'link_close', + level: --level + }); + text = text.slice(pos + links[ln].text.length); + } + if (text.length) { + nodes.push({ + type: 'text', + content: text, + level: level + }); + } + + // replace current node + blockTokens[j].children = tokens = [].concat(tokens.slice(0, i), nodes, tokens.slice(i + 1)); + } + } + } +}; diff --git a/lib/rules_core/references.js b/lib/rules_core/references.js new file mode 100644 index 0000000..cb03c59 --- /dev/null +++ b/lib/rules_core/references.js @@ -0,0 +1,28 @@ +'use strict'; + +var parseRef = require('../parser_ref'); + +module.exports = function references(state) { + var tokens = state.tokens, i, l, content, pos; + + // Parse inlines + for (i = 1, l = tokens.length - 1; i < l; i++) { + if (tokens[i - 1].type === 'paragraph_open' && + tokens[i].type === 'inline' && + tokens[i + 1].type === 'paragraph_close') { + + content = tokens[i].content; + while (content.length) { + pos = parseRef(content, state.inline, state.options, state.env); + if (pos < 0) { break; } + content = content.slice(pos).trim(); + } + + tokens[i].content = content; + if (!content.length) { + tokens[i - 1].tight = true; + tokens[i + 1].tight = true; + } + } + } +}; diff --git a/lib/rules_core/typographer.js b/lib/rules_core/typographer.js new file mode 100644 index 0000000..54c8f4a --- /dev/null +++ b/lib/rules_core/typographer.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = function typographer(state) { + if (!state.options.typographer) { return; } + var tokens = state.tokens, tok, i, l; + + // Parse inlines + for (i = 0, l = tokens.length; i < l; i++) { + tok = tokens[i]; + if (tok.type === 'inline') { + state.typographer.process(tok, state); + } + } +}; diff --git a/lib/rules_text/linkify.js b/lib/rules_text/linkify.js deleted file mode 100644 index 414294f..0000000 --- a/lib/rules_text/linkify.js +++ /dev/null @@ -1,120 +0,0 @@ -// Replace link-like texts with link nodes. -// -// Currently restricted to http/https/ftp -// -'use strict'; - - -var Autolinker = require('autolinker'); - - -var LINK_SCAN_RE = /www|\:\/\//; - -var links = []; -var autolinker = new Autolinker({ - stripPrefix: false, - replaceFn: function (autolinker, match) { - // Only collect matched strings but don't change anything. - if (match.getType() === 'url') { - links.push({ text: match.matchedText, url: match.getUrl() }); - } - return false; - } -}); - -function isLinkOpen(str) { - return /^\s]/i.test(str); -} -function isLinkClose(str) { - return /^<\/a\s*>/i.test(str); -} - - -module.exports = function linkify(t, state) { - var i, token, text, nodes, ln, pos, level, - htmlLinkLevel = 0, - tokens = state.tokens; - - // We scan from the end, to keep position when new tags added. - // Use reversed logic in links start/end match - for (i = tokens.length - 1; i >= 0; i--) { - token = tokens[i]; - - // Skip content of markdown links - if (token.type === 'link_close') { - i--; - while (tokens[i].level !== token.level && tokens[i].type !== 'link_open') { - i--; - } - continue; - } - - // Skip content of html tag links - if (token.type === 'htmltag') { - if (isLinkOpen(token.content) && htmlLinkLevel > 0) { - htmlLinkLevel--; - } - if (isLinkClose(token.content)) { - htmlLinkLevel++; - } - } - if (htmlLinkLevel > 0) { continue; } - - if (token.type === 'text' && LINK_SCAN_RE.test(token.content)) { - - text = token.content; - links.length = 0; - autolinker.link(text); - - if (!links.length) { continue; } - - // Now split string to nodes - nodes = []; - level = token.level; - - for (ln = 0; ln < links.length; ln++) { - - if (!state.parser.validateLink(links[ln].url)) { continue; } - - pos = text.indexOf(links[ln].text); - - if (pos === -1) { continue; } - - if (pos) { - level = level; - nodes.push({ - type: 'text', - content: text.slice(0, pos), - level: level - }); - } - nodes.push({ - type: 'link_open', - href: links[ln].url, - title: '', - level: level++ - }); - nodes.push({ - type: 'text', - content: links[ln].text, - level: level - }); - nodes.push({ - type: 'link_close', - level: --level - }); - text = text.slice(pos + links[ln].text.length); - } - if (text.length) { - nodes.push({ - type: 'text', - content: text, - level: level - }); - } - - // replace cuttent node - state.tokens = tokens = [].concat(tokens.slice(0, i), nodes, tokens.slice(i + 1)); - } - } -}; diff --git a/lib/rules_text/replace.js b/lib/rules_text/replace.js index f311631..88d5c39 100644 --- a/lib/rules_text/replace.js +++ b/lib/rules_text/replace.js @@ -6,10 +6,10 @@ var COPY_RE = /\((c|tm|r|p)\)/i; var RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; -module.exports = function replace(t, state) { +module.exports = function replace(typographer, blockToken) { var i, token, text, - tokens = state.tokens, - options = t.options; + tokens = blockToken.children, + options = typographer.options; for (i = tokens.length - 1; i >= 0; i--) { token = tokens[i]; diff --git a/lib/rules_text/smartquotes.js b/lib/rules_text/smartquotes.js index 3482412..8febc38 100644 --- a/lib/rules_text/smartquotes.js +++ b/lib/rules_text/smartquotes.js @@ -22,11 +22,11 @@ function replaceAt(str, index, ch) { var stack = []; -module.exports = function smartquotes(typographer, state) { +module.exports = function smartquotes(typographer, blockToken) { /*eslint max-depth:0*/ var i, token, text, t, pos, max, thisLevel, lastSpace, nextSpace, item, canOpen, canClose, j, isSingle, chars, options = typographer.options, - tokens = state.tokens; + tokens = blockToken.children; stack.length = 0; diff --git a/lib/typographer.js b/lib/typographer.js index e2107c9..dd549e1 100644 --- a/lib/typographer.js +++ b/lib/typographer.js @@ -33,13 +33,13 @@ Typographer.prototype.set = function (options) { }; -Typographer.prototype.process = function (state) { +Typographer.prototype.process = function (token) { var i, l, rules; rules = this.ruler.getRules(''); for (i = 0, l = rules.length; i < l; i++) { - rules[i](this, state); + rules[i](this, token); } };