From 73905030660e076bdcb44f2504ca1d46bd3243d7 Mon Sep 17 00:00:00 2001 From: Vitaly Puzrin Date: Mon, 6 Oct 2014 23:34:05 +0400 Subject: [PATCH] Moved rules manager to separate class (block parser only) --- benchmark/profile.js | 2 +- lib/parser_block.js | 115 ++++++------------------ lib/ruler.js | 160 ++++++++++++++++++++++++++++++++++ lib/rules_block/blockquote.js | 22 +++-- lib/rules_block/list.js | 13 ++- lib/rules_block/paragraph.js | 23 +++-- 6 files changed, 216 insertions(+), 119 deletions(-) mode change 100644 => 100755 benchmark/profile.js create mode 100644 lib/ruler.js diff --git a/benchmark/profile.js b/benchmark/profile.js old mode 100644 new mode 100755 index 08e41e5..789594a --- a/benchmark/profile.js +++ b/benchmark/profile.js @@ -6,7 +6,7 @@ var Remarkable = require('../'); var md = new Remarkable(); -var data = fs.readFileSync(path.join(__dirname, '/samples/lorem1.txt', 'utf8')); +var data = fs.readFileSync(path.join(__dirname, '/samples/lorem1.txt'), 'utf8'); for (var i = 0; i < 20000; i++) { md.render(data); diff --git a/lib/parser_block.js b/lib/parser_block.js index 5aa41e0..04bf035 100644 --- a/lib/parser_block.js +++ b/lib/parser_block.js @@ -4,7 +4,9 @@ 'use strict'; +var Ruler = require('./ruler'); var State = require('./rules_block/state_block'); + var skipEmptyLines = require('./helpers').skipEmptyLines; var isEmpty = require('./helpers').isEmpty; @@ -12,102 +14,39 @@ var isEmpty = require('./helpers').isEmpty; var rules = []; // `list` should be after `hr`, but before `heading` -rules.push(require('./rules_block/code')); -rules.push(require('./rules_block/fences')); -rules.push(require('./rules_block/blockquote')); -rules.push(require('./rules_block/hr')); -rules.push(require('./rules_block/list')); -rules.push(require('./rules_block/heading')); -rules.push(require('./rules_block/lheading')); -rules.push(require('./rules_block/htmlblock')); -rules.push(require('./rules_block/table')); -rules.push(require('./rules_block/paragraph')); - - -function functionName(fn) { - var ret = fn.toString(); - ret = ret.substr('function '.length); - ret = ret.substr(0, ret.indexOf('(')); - return ret; -} - -function findByName(self, name) { - for (var i = 0; i < self.rules.length; i++) { - if (functionName(self.rules[i]) === name) { - return i; - } - } - return -1; -} +rules.push([ require('./rules_block/code') ]); +rules.push([ require('./rules_block/fences'), 'paragraph', 'blockquote', 'list' ]); +rules.push([ require('./rules_block/blockquote'), 'paragraph', 'blockquote', 'list' ]); +rules.push([ require('./rules_block/hr'), 'paragraph', 'blockquote', 'list' ]); +rules.push([ require('./rules_block/list'), 'paragraph', 'blockquote' ]); +rules.push([ require('./rules_block/heading'), 'paragraph', 'blockquote' ]); +rules.push([ require('./rules_block/lheading') ]); +rules.push([ require('./rules_block/htmlblock'), 'paragraph', 'blockquote' ]); +rules.push([ require('./rules_block/table'), 'paragraph' ]); +rules.push([ require('./rules_block/paragraph') ]); // Block Parser class // function ParserBlock() { - this.rules = []; - this.rules_named = {}; + this._rules = []; + this._rulesParagraphTerm = []; + this._rulesBlockquoteTerm = []; + this._rulesListTerm = []; + + this.ruler = new Ruler(this.rulesUpdate.bind(this)); for (var i = 0; i < rules.length; i++) { - this.after(null, rules[i]); + this.ruler.after(rules[i][0], rules[i].slice(1)); } } -// Replace/delete parser function -// -ParserBlock.prototype.at = function (name, fn) { - var index = findByName(name); - if (index === -1) { - throw new Error('Parser rule not found: ' + name); - } - - if (fn) { - this.rules[index] = fn; - } else { - this.rules = this.rules.slice(0, index).concat(this.rules.slice(index + 1)); - } - - this.rules_named[functionName(fn)] = fn; -}; - - -// Add function to parser chain before one with given name. -// Or add to start, if name not defined -// -ParserBlock.prototype.before = function (name, fn) { - if (!name) { - this.rules.unshift(fn); - this.rules_named[functionName(fn)] = fn; - return; - } - - var index = findByName(name); - if (index === -1) { - throw new Error('Parser rule not found: ' + name); - } - - this.rules.splice(index, 0, fn); - this.rules_named[functionName(fn)] = fn; -}; - - -// Add function to parser chain after one with given name. -// Or add to end, if name not defined -// -ParserBlock.prototype.after = function (name, fn) { - if (!name) { - this.rules.push(fn); - this.rules_named[functionName(fn)] = fn; - return; - } - - var index = findByName(name); - if (index === -1) { - throw new Error('Parser rule not found: ' + name); - } - - this.rules.splice(index + 1, 0, fn); - this.rules_named[functionName(fn)] = fn; +ParserBlock.prototype.rulesUpdate = function () { + this._rules = this.ruler.getRules(); + this._rulesParagraphTerm = this.ruler.getRules('paragraph'); + this._rulesBlockquoteTerm = this.ruler.getRules('blockquote'); + this._rulesListTerm = this.ruler.getRules('list'); }; @@ -115,8 +54,8 @@ ParserBlock.prototype.after = function (name, fn) { // ParserBlock.prototype.tokenize = function (state, startLine, endLine) { var ok, i, - rules = this.rules, - len = this.rules.length, + rules = this._rules, + len = this._rules.length, line = startLine, hasEmptyLines = false; @@ -201,7 +140,6 @@ ParserBlock.prototype.parse = function (src, options, env) { }); } - state = new State( src, this, @@ -213,7 +151,6 @@ ParserBlock.prototype.parse = function (src, options, env) { this.tokenize(state, state.line, state.lineMax); return state.tokens; - }; diff --git a/lib/ruler.js b/lib/ruler.js new file mode 100644 index 0000000..879956b --- /dev/null +++ b/lib/ruler.js @@ -0,0 +1,160 @@ +// Ruler is helper class to build responsibility chains from parse rules. +// It allows: +// +// - easy stack rules chains +// - getting main chain and named chains content (as arrays of functions) + +'use strict'; + + +//////////////////////////////////////////////////////////////////////////////// +// helpers + +function _class(obj) { return Object.prototype.toString.call(obj); } +function isFunction(obj) { return _class(obj) === '[object Function]'; } + +function functionName(fn) { + var ret = fn.toString(); + ret = ret.substr('function '.length); + ret = ret.substr(0, ret.indexOf('(')); + return ret; +} + + +//////////////////////////////////////////////////////////////////////////////// + +function Ruler(compileFn) { + this.compile = compileFn; // callback to call after each change + + // List of added rules. Each element is: + // + // { + // name: XXX, + // fn: Function(), + // alt: [ name2, name3 ] + // } + // + this.rules = []; +} + + +// Find rule index by name +// +Ruler.prototype.find = function (name) { + for (var i = 0; i < this.rules.length; i++) { + if (this.rules[i].name === name) { + return i; + } + } + return -1; +}; + + +// Replace/delete parser function +// +Ruler.prototype.at = function (name, fn, altNames) { + var index = this.find(name); + + if (index === -1) { + throw new Error('Parser rule not found: ' + name); + } + + if (isFunction(fn)) { + this.rules[index].fn = fn; + if (altNames) { + this.rules[index].alt = altNames; + } + } else { + this.rules = this.rules.slice(0, index).concat(this.rules.slice(index + 1)); + } + + this.compile(); +}; + + +// Add function to parser chain before one with given name. +// Or add to start, if name not defined +// +Ruler.prototype.before = function (name, fn, altNames) { + var index; + + if (isFunction(name)) { + altNames = fn; + fn = name; + name = ''; + } + + if (!name) { + this.rules.unshift({ + name: functionName(fn), + fn: fn, + alt: altNames || [] + }); + + } else { + + index = this.find(name); + if (index === -1) { + throw new Error('Parser rule not found: ' + name); + } + this.rules.splice(index, 0, fn); + } + + this.compile(); +}; + + +// Add function to parser chain after one with given name. +// Or add to end, if name not defined +// +Ruler.prototype.after = function (name, fn, altNames) { + var index; + + if (isFunction(name)) { + altNames = fn; + fn = name; + name = ''; + } + + if (!name) { + this.rules.push({ + name: functionName(fn), + fn: fn, + alt: altNames || [] + }); + + } else { + + index = this.find(name); + if (index === -1) { + throw new Error('Parser rule not found: ' + name); + } + this.rules.splice(index + 1, 0, fn); + } + + this.compile(); +}; + + +// Get rules list as array of functions. By default returns main chain +// +Ruler.prototype.getRules = function (chainName) { + var result = []; + + if (!chainName) { + this.rules.forEach(function (rule) { + result.push(rule.fn); + }); + return result; + } + + this.rules.forEach(function (rule) { + if (rule.alt.indexOf(chainName) >= 0) { + result.push(rule.fn); + } + }); + return result; +}; + + +module.exports = Ruler; diff --git a/lib/rules_block/blockquote.js b/lib/rules_block/blockquote.js index cba74ac..ec68892 100644 --- a/lib/rules_block/blockquote.js +++ b/lib/rules_block/blockquote.js @@ -7,8 +7,8 @@ var skipSpaces = require('../helpers').skipSpaces; module.exports = function blockquote(state, startLine, endLine, silent) { - var nextLine, lastLineEmpty, oldTShift, oldBMarks, i, oldIndent, oldListMode, - rules_named = state.parser.rules_named, + var nextLine, lastLineEmpty, oldTShift, oldBMarks, oldIndent, oldListMode, + terminatorRules = state.parser._rulesBlockquoteTerm, i, l, terminate, pos = state.bMarks[startLine] + state.tShift[startLine], max = state.eMarks[startLine]; @@ -88,16 +88,14 @@ module.exports = function blockquote(state, startLine, endLine, silent) { if (lastLineEmpty) { break; } // Case 3: another tag found. - if (rules_named.fences(state, nextLine, endLine, true)) { break; } - if (rules_named.hr(state, nextLine, endLine, true)) { break; } - if (rules_named.list(state, nextLine, endLine, true)) { break; } - if (rules_named.heading(state, nextLine, endLine, true)) { break; } - // setex header can't interrupt paragraph - // if (rules_named.lheading(state, nextLine, endLine, true)) { break; } - if (rules_named.blockquote(state, nextLine, endLine, true)) { break; } - if (rules_named.table(state, nextLine, endLine, true)) { break; } - //if (rules_named.tag(state, nextLine, endLine, true)) { break; } - //if (rules_named.def(state, nextLine, endLine, true)) { break; } + terminate = false; + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true; + break; + } + } + if (terminate) { break; } oldBMarks.push(state.bMarks[nextLine]); oldTShift.push(state.tShift[nextLine]); diff --git a/lib/rules_block/list.js b/lib/rules_block/list.js index 69ab394..7faddfc 100644 --- a/lib/rules_block/list.js +++ b/lib/rules_block/list.js @@ -90,7 +90,7 @@ module.exports = function list(state, startLine, endLine, silent) { contentStart, listTokIdx, prevEmptyEnd, - rules_named = state.parser.rules_named; + terminatorRules = state.parser._rulesListTerm, i, l, terminate; // Detect list type and position after marker if ((posAfterMarker = skipOrderedListMarker(state, startLine)) >= 0) { @@ -210,9 +210,14 @@ module.exports = function list(state, startLine, endLine, silent) { if (state.bqMarks[nextLine] < state.bqLevel) { break; } // fail if terminating block found - if (rules_named.fences(state, nextLine, endLine, true)) { break; } - if (rules_named.blockquote(state, nextLine, endLine, true)) { break; } - if (rules_named.hr(state, nextLine, endLine, true)) { break; } + terminate = false; + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true; + break; + } + } + if (terminate) { break; } // fail if list has another type if (isOrdered) { diff --git a/lib/rules_block/paragraph.js b/lib/rules_block/paragraph.js index be24aa4..25d92b7 100644 --- a/lib/rules_block/paragraph.js +++ b/lib/rules_block/paragraph.js @@ -9,9 +9,9 @@ var parseRef = require('../parser_ref'); module.exports = function paragraph(state, startLine/*, endLine*/) { - var endLine, content, pos, + var endLine, content, pos, terminate, i, l, nextLine = startLine + 1, - rules_named = state.parser.rules_named; + terminatorRules = state.parser._rulesParagraphTerm; endLine = state.lineMax; @@ -22,17 +22,14 @@ module.exports = function paragraph(state, startLine/*, endLine*/) { if (state.tShift[nextLine] - state.blkIndent > 3) { continue; } // Some tags can terminate paragraph without empty line. - if (rules_named.fences(state, nextLine, endLine, true)) { break; } - if (rules_named.hr(state, nextLine, endLine, true)) { break; } - if (rules_named.list(state, nextLine, endLine, true)) { break; } - if (rules_named.heading(state, nextLine, endLine, true)) { break; } - // setex header can't interrupt paragraph - // if (rules_named.lheading(state, nextLine, endLine, true)) { break; } - if (rules_named.blockquote(state, nextLine, endLine, true)) { break; } - if (rules_named.htmlblock(state, nextLine, endLine, true)) { break; } - if (rules_named.table(state, nextLine, endLine, true)) { break; } - //if (rules_named.tag(state, nextLine, endLine, true)) { break; } - //if (rules_named.def(state, nextLine, endLine, true)) { break; } + terminate = false; + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true; + break; + } + } + if (terminate) { break; } } content = getLines(state, startLine, nextLine, state.blkIndent, false).trim();