Browse Source

Code structure and options refactoring

pull/14/head
Vitaly Puzrin 10 years ago
parent
commit
b27c630815
  1. 21
      README.md
  2. 4
      benchmark/implementations/current/index.js
  3. 7
      benchmark/profile.js
  4. 5
      bin/remarkable.js
  5. 2
      demo/assets/index.js
  6. 17
      demo/sample.js
  7. 14
      lib/defaults.js
  8. 19
      lib/defaults/commonmark.js
  9. 26
      lib/defaults/commonmark_rules.js
  10. 19
      lib/defaults/remarkable.js
  11. 18
      lib/defaults/typographer.js
  12. 3
      lib/defaults_typographer.js
  13. 24
      lib/index.js
  14. 5
      lib/parser_inline.js
  15. 8
      lib/renderer.js
  16. 93
      lib/ruler.js
  17. 2
      lib/rules_block/blockquote.js
  18. 2
      lib/rules_block/list.js
  19. 2
      lib/rules_inline/emphasis.js
  20. 2
      lib/rules_inline/links.js
  21. 2
      lib/rules_inline/newline.js
  22. 98
      lib/rules_typographer/linkify.js
  23. 52
      lib/rules_typographer/replace.js
  24. 151
      lib/typographer.js
  25. 7
      support/specsplit.js
  26. 4
      test/remarkable.js
  27. 1
      test/remarked.js
  28. 7
      test/stmd.js

21
README.md

@ -32,19 +32,22 @@ Usage
```javascript
var Remarkable = require('remarkable');
var md = new Remarkable({
html: false, // enable html tags in source
xhtml: false, // use '/' to close single tags (<br />)
breaks: true, // convert '\n' in paragraphs into <br>
langPrefix: 'language-', // css language prefix for fenced blocks
// Should return HTML markup for highlighted text,
// or empty string to escape source
highlight: function (str, lang) { return ''; }
html: false, // Enable html tags in source
xhtmlOut: false, // Use '/' to close single tags (<br />)
breaks: false, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks
linkify: false, // autoconvert url-like texts to links
typographer: false, // Enable smartypants and other sweet transforms
// Highlighter function. Should return escaped html,
// or '' if input not changed
highlight: function (/*str, , lang*/) { return ''; }
});
console.log(md.parse('# Remarkable rulezz!'));
//=> <h1># Remarkable rulezz!</h1>
// => <h1>Remarkable rulezz!</h1>
```
You can define options via `set` method:

4
benchmark/implementations/current/index.js

@ -1,12 +1,12 @@
'use strict'
var Remarkable = require('../../../');
var md = new Remarkable({
var md = new Remarkable('commonmark' /*{
html: true,
xhtml: true,
breaks: false,
langPrefix: 'language-'
});
}*/);
exports.run = function(data) {
return md.render(data);

7
benchmark/profile.js

@ -6,12 +6,7 @@ var fs = require('fs');
var path = require('path');
var Remarkable = require('../');
var md = new Remarkable({
html: true,
xhtml: true,
breaks: false,
langPrefix: 'language-'
});
var md = new Remarkable('commonmark');
var data = fs.readFileSync(path.join(__dirname, '/samples/lorem1.txt'), 'utf8');

5
bin/remarkable.js

@ -63,8 +63,9 @@ readFile(options.file, 'utf8', function (error, input) {
md = new Remarkable({
html: true,
xhtml: true,
typographer: true
xhtmlOut: true,
typographer: true,
linkify: true
});
try {

2
demo/assets/index.js

@ -5,7 +5,7 @@
var defaults = {
html: true,
xhtml: true,
xhtmlOut: true,
breaks: false,
langPrefix: 'language-',

17
demo/sample.js

@ -1,13 +1,16 @@
var Remarkable = require('remarkable');
var md = new Remarkable({
html: false, // enable html tags in source
xhtml: false, // use '/' to close single tags (<br />)
breaks: true, // convert '\n' in paragraphs into <br>
langPrefix: 'language-', // css language prefix for fenced blocks
html: false, // Enable html tags in source
xhtmlOut: false, // Use '/' to close single tags (<br />)
breaks: false, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks
linkify: false, // autoconvert url-like texts to links
typographer: false, // Enable smartypants and other sweet transforms
// Should return HTML markup for highlighted text,
// or empty string to escape source
highlight: function (str, lang) { return ''; }
// Highlighter function. Should return escaped html,
// or '' if input not changed
highlight: function (/*str, , lang*/) { return ''; }
});
console.log(md.parse('# Remarkable rulezz!'));

14
lib/defaults.js

@ -1,14 +0,0 @@
// Default options
'use strict';
module.exports = {
html: false,
xhtml: false,
breaks: false,
maxLevel: 20,
langPrefix: 'language-',
typograph: false,
highlight: function (/*str*/) { return ''; }
};

19
lib/defaults/commonmark.js

@ -0,0 +1,19 @@
// Commonmark default options
'use strict';
module.exports = {
html: true, // Enable html tags in source
xhtmlOut: true, // Use '/' to close single tags (<br />)
breaks: false, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks
linkify: false, // autoconvert url-like texts to links
typographer: false, // Enable smartypants and other sweet transforms
// Highlighter function. Should return escaped html,
// or '' if input not changed
highlight: function (/*str, , lang*/) { return ''; },
maxNesting: 20 // Internal protection, recursion limit
};

26
lib/defaults/commonmark_rules.js

@ -0,0 +1,26 @@
// List of active rules for strict commonmark mode
module.exports.block = [
'code',
'blockquote',
'fences',
'heading',
'hr',
'htmlblock',
'lheading',
'list',
'paragraph'
];
module.exports.inline = [
'autolink',
'backticks',
'emphasis',
'entity',
'escape',
'escape_html_char',
'htmltag',
'links',
'newline',
'text'
];

19
lib/defaults/remarkable.js

@ -0,0 +1,19 @@
// Remarkable default options
'use strict';
module.exports = {
html: false, // Enable html tags in source
xhtmlOut: false, // Use '/' to close single tags (<br />)
breaks: false, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks
linkify: false, // autoconvert url-like texts to links
typographer: false, // Enable smartypants and other sweet transforms
// Highlighter function. Should return escaped html,
// or '' if input not changed
highlight: function (/*str, , lang*/) { return ''; },
maxNesting: 20 // Internal protection, recursion limit
};

18
lib/defaults/typographer.js

@ -0,0 +1,18 @@
// Default typograph options
'use strict';
module.exports = {
singleQuotes: '‘’',
doubleQuotes: '“”', // «» - russian, „“ - deutch
copyright: true,
trademark: true,
registered: true,
plusminus: true,
paragraph: true,
ellipsis: true,
dupes: true,
emDashes: true,
linkify: true
};

3
lib/defaults_typographer.js

@ -13,6 +13,5 @@ module.exports = {
paragraph: true,
ellipsis: true,
dupes: true,
emDashes: true,
linkify: true
emDashes: true
};

24
lib/index.js

@ -8,7 +8,12 @@ var Renderer = require('./renderer');
var ParserBlock = require('./parser_block');
var ParserInline = require('./parser_inline');
var Typographer = require('./typographer');
var defaults = require('./defaults');
var defaults = require('./defaults/remarkable');
var cmmDefaults = require('./defaults/commonmark');
var cmmRules = require('./defaults/commonmark_rules');
// Main class
//
@ -20,17 +25,30 @@ function Remarkable(options) {
this.block = new ParserBlock();
this.renderer = new Renderer();
this.typographer = new Typographer();
this.linkifier = new Typographer();
// Linkifier is a separate typographer, for convenience.
// Configure it here.
this.linkifier.ruler.enable([], true);
this.linkifier.ruler.after(require('./rules_typographer/linkify'));
// Cross-references to simplify code (a bit dirty, but easy).
this.block.inline = this.inline;
this.block.inline = this.inline;
this.inline.typographer = this.typographer;
this.inline.linkifier = this.linkifier;
if (options) { this.set(options); }
}
Remarkable.prototype.set = function (options) {
assign(this.options, options);
if (String(options).toLowerCase() === 'commonmark') {
assign(this.options, cmmDefaults);
this.inline.ruler.enable(cmmRules.inline, true);
this.block.ruler.enable(cmmRules.block, true);
} else {
assign(this.options, options);
}
};

5
lib/parser_inline.js

@ -111,7 +111,10 @@ ParserInline.prototype.parse = function (str, options, env) {
this.tokenize(state);
if (options.typographer && this.typographer) {
if (options.linkify) {
this.linkifier.process(state);
}
if (options.typographer) {
this.typographer.process(state);
}

8
lib/renderer.js

@ -81,7 +81,7 @@ rules.heading_close = function (tokens, idx /*, options*/) {
rules.hr = function (tokens, idx, options) {
return (options.xhtml ? '<hr />' : '<hr>') + getBreak(tokens, idx);
return (options.xhtmlOut ? '<hr />' : '<hr>') + getBreak(tokens, idx);
};
@ -129,7 +129,7 @@ rules.image = function (tokens, idx, options) {
var src = ' src="' + escapeHtml(escapeUrl(tokens[idx].src)) + '"';
var title = tokens[idx].title ? (' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"') : '';
var alt = ' alt="' + (tokens[idx].alt ? escapeHtml(replaceEntities(tokens[idx].alt)) : '') + '"';
var suffix = options.xhtml ? ' /' : '';
var suffix = options.xhtmlOut ? ' /' : '';
return '<img' + src + alt + title + suffix + '>';
};
@ -189,10 +189,10 @@ rules.del_close = function(/*tokens, idx, options*/) {
rules.hardbreak = function (tokens, idx, options) {
return options.xhtml ? '<br />\n' : '<br>\n';
return options.xhtmlOut ? '<br />\n' : '<br>\n';
};
rules.softbreak = function (tokens, idx, options) {
return options.breaks ? (options.xhtml ? '<br />\n' : '<br>\n') : '\n';
return options.breaks ? (options.xhtmlOut ? '<br />\n' : '<br>\n') : '\n';
};

93
lib/ruler.js

@ -3,7 +3,7 @@
//
// - easy stack rules chains
// - getting main chain and named chains content (as arrays of functions)
//
'use strict';
@ -30,6 +30,7 @@ function Ruler(compileFn) {
//
// {
// name: XXX,
// enabled: Boolean,
// fn: Function(),
// alt: [ name2, name3 ]
// }
@ -76,7 +77,7 @@ Ruler.prototype.at = function (name, fn, altNames) {
// Or add to start, if name not defined
//
Ruler.prototype.before = function (name, fn, altNames) {
var index;
var index, rule;
if (isFunction(name)) {
altNames = fn;
@ -84,20 +85,21 @@ Ruler.prototype.before = function (name, fn, altNames) {
name = '';
}
if (!name) {
this.rules.unshift({
name: functionName(fn),
fn: fn,
alt: altNames || []
});
rule = {
name: functionName(fn),
enabled: true,
fn: fn,
alt: altNames || []
};
if (!name) {
this.rules.unshift(rule);
} else {
index = this.find(name);
if (index === -1) {
throw new Error('Parser rule not found: ' + name);
}
this.rules.splice(index, 0, fn);
this.rules.splice(index, 0, rule);
}
this.compile();
@ -108,7 +110,7 @@ Ruler.prototype.before = function (name, fn, altNames) {
// Or add to end, if name not defined
//
Ruler.prototype.after = function (name, fn, altNames) {
var index;
var index, rule;
if (isFunction(name)) {
altNames = fn;
@ -116,15 +118,16 @@ Ruler.prototype.after = function (name, fn, altNames) {
name = '';
}
if (!name) {
this.rules.push({
name: functionName(fn),
fn: fn,
alt: altNames || []
});
rule = {
name: functionName(fn),
enabled: true,
fn: fn,
alt: altNames || []
};
if (!name) {
this.rules.push(rule);
} else {
index = this.find(name);
if (index === -1) {
throw new Error('Parser rule not found: ' + name);
@ -143,13 +146,15 @@ Ruler.prototype.getRules = function (chainName) {
if (!chainName) {
this.rules.forEach(function (rule) {
result.push(rule.fn);
if (rule.enabled) {
result.push(rule.fn);
}
});
return result;
}
this.rules.forEach(function (rule) {
if (rule.alt.indexOf(chainName) >= 0) {
if (rule.alt.indexOf(chainName) >= 0 && rule.enabled) {
result.push(rule.fn);
}
});
@ -157,4 +162,52 @@ Ruler.prototype.getRules = function (chainName) {
};
// Enable list of rules by names. If `strict` is true, then all non listed
// rules will be disabled.
//
Ruler.prototype.enable = function (list, strict) {
if (!Array.isArray(list)) {
list = [ list ];
}
// In strict mode disable all existing rules first
if (strict) {
this.rules.forEach(function (rule) {
rule.enabled = false;
});
}
// Search by name and enable
list.forEach(function (name) {
var idx = this.find(name);
if (idx < 0) { throw new Error('Rules namager: invalid rule name ' + name);}
this.rules[idx].enabled = true;
}, this);
this.compile();
};
// Disable list of rules by names.
//
Ruler.prototype.disable = function (list) {
if (!Array.isArray(list)) {
list = [ list ];
}
// Search by name and disable
list.forEach(function (name) {
var idx = this.find(name);
if (idx < 0) { throw new Error('Rules namager: invalid rule name ' + name);}
this.rules[idx].enabled = false;
}, this);
this.compile();
};
module.exports = Ruler;

2
lib/rules_block/blockquote.js

@ -17,7 +17,7 @@ module.exports = function blockquote(state, startLine, endLine, silent) {
// check the block quote marker
if (state.src.charCodeAt(pos++) !== 0x3E/* > */) { return false; }
if (state.level >= state.options.maxLevel) { return false; }
if (state.level >= state.options.maxNesting) { return false; }
// we know that it's going to be a valid blockquote,
// so no point trying to find the end of it in silent mode

2
lib/rules_block/list.js

@ -101,7 +101,7 @@ module.exports = function list(state, startLine, endLine, silent) {
return false;
}
if (state.level >= state.options.maxLevel) { return false; }
if (state.level >= state.options.maxNesting) { return false; }
// We should terminate list on style change. Remember first one to compare.
markerCharCode = state.src.charCodeAt(posAfterMarker - 1);

2
lib/rules_inline/emphasis.js

@ -124,7 +124,7 @@ module.exports = function emphasis(state/*, silent*/) {
return true;
}
if (state.level >= state.options.maxLevel) { return false; }
if (state.level >= state.options.maxNesting) { return false; }
oldLength = state.tokens.length;
oldPending = state.pending;

2
lib/rules_inline/links.js

@ -28,7 +28,7 @@ function links(state) {
}
if (marker !== 0x5B/* [ */) { return false; }
if (state.level >= state.options.maxLevel) { return false; }
if (state.level >= state.options.maxNesting) { return false; }
labelStart = start + 1;
labelEnd = parseLinkLabel(state, start);

2
lib/rules_inline/newline.js

@ -1,6 +1,6 @@
// Proceess '\n'
module.exports = function escape(state) {
module.exports = function newline(state) {
var pmax, max, pos = state.pos;
if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; }

98
lib/rules_typographer/linkify.js

@ -0,0 +1,98 @@
// Replace link-like texts with link nodes.
//
// Currently restricted to http/https/ftp
//
'use strict';
var Autolinker = require('autolinker');
var escapeHtml = require('../helpers').escapeHtml;
var links = [];
var autolinker = new Autolinker({
stripPrefix: false,
replaceFn: function (autolinker, match) {
// Only collect matched strings but don't change anything.
var url;
if (match.getType() === 'url') {
url = match.getUrl();
if (/^(http|https|ftp|git)/.test(url)) {
links.push(url);
}
}
return false;
}
});
module.exports = function linkify(t, state) {
var i, token, text, nodes, ln, pos, level,
tokens = state.tokens;
for (i = tokens.length - 1; i >= 0; i--) {
token = tokens[i];
// Skip content of links
if (token.type === 'link_close') {
i--;
while (tokens[i].type !== 'link_open' && tokens[i].level !== token.level) {
i--;
}
i--;
continue;
}
if (token.type === 'text' &&
(token.content.indexOf('://') ||
token.content.indexOf('www'))) {
text = token.content;
links = [];
autolinker.link(text);
if (!links.length) { continue; }
// Now split string to nodes
nodes = [];
level = token.level;
for (ln = 0; ln < links.length; ln++) {
pos = text.indexOf(links[ln]);
if (pos) {
level = level;
nodes.push({
type: 'text',
content: text.slice(0, pos),
level: level
});
}
nodes.push({
type: 'link_open',
href: links[ln],
title: '',
level: level++
});
nodes.push({
type: 'text',
content: escapeHtml(links[ln]),
level: level
});
nodes.push({
type: 'link_close',
level: --level
});
text = text.slice(pos + links[ln].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));
}
}
};

52
lib/rules_typographer/replace.js

@ -0,0 +1,52 @@
// Simple typographyc replacements
//
'use strict';
module.exports = function replace(t, state) {
var i, token, text,
tokens = state.tokens,
options = t.options;
for (i = tokens.length - 1; i >= 0; i--) {
token = tokens[i];
if (token.type === 'text') {
text = token.content;
if (text.indexOf('(') >= 0) {
if (options.copyright) {
text = text.replace(/\(c\)/gi, '©');
}
if (options.trademark) {
text = text.replace(/\(tm\)/gi, '™');
}
if (options.registered) {
text = text.replace(/\(r\)/gi, '®');
}
if (options.paragraph) {
text = text.replace(/\(p\)/gi, '§');
}
}
if (options.plusminus && text.indexOf('+-') >= 0) {
text = text.replace(/\+-/g, '±');
}
if (options.ellipsis && text.indexOf('..') >= 0) {
// .., ..., ....... -> …
// but ?..... & !..... -> ?.. & !..
text = text.replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..');
}
if (options.dupes &&
(text.indexOf('????') >= 0 ||
text.indexOf('!!!!') >= 0 ||
text.indexOf(',,') >= 0)) {
text = text.replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ',');
}
if (options.emDashes && text.indexOf('--') >= 0) {
text = text.replace(/(^|\s)--(\s|$)/mg, '$1—$2');
}
token.content = text;
}
}
};

151
lib/typographer.js

@ -1,11 +1,5 @@
// Class of typographic replacements rules
//
// - single quotes
// - double quotes
// - em-dashes
// - link patterns
// - email patterns
//
'use strict';
// TODO:
@ -13,156 +7,15 @@
// - miltiplication 2 x 4 -> 2 × 4
var Autolinker = require('autolinker');
var defaults = require('./defaults/typographer');
var assign = require('./common/utils').assign;
var escapeHtml = require('./helpers').escapeHtml;
var defaults = require('./defaults_typographer');
var Ruler = require('./ruler');
var links = [];
var autolinker = new Autolinker({
stripPrefix: false,
replaceFn: function (autolinker, match) {
// Only collect matched strings but don't change anything.
var url;
if (match.getType() === 'url') {
url = match.getUrl();
if (/^(http|https|ftp|git)/.test(url)) {
links.push(url);
}
}
return false;
}
});
var rules = [];
rules.push(function linkify(t, state) {
var i, token, text, nodes, ln, pos, level,
tokens = state.tokens;
if (!t.options.linkify) { return; }
for (i = tokens.length - 1; i >= 0; i--) {
token = tokens[i];
// Skip content of links
if (token.type === 'link_close') {
i--;
while (tokens[i].type !== 'link_open' && tokens[i].level !== token.level) {
i--;
}
i--;
continue;
}
if (token.type === 'text' &&
(token.content.indexOf('://') ||
token.content.indexOf('www'))) {
text = token.content;
links = [];
autolinker.link(text);
if (!links.length) { continue; }
// Now split string to nodes
nodes = [];
level = token.level;
for (ln = 0; ln < links.length; ln++) {
pos = text.indexOf(links[ln]);
if (pos) {
level = level;
nodes.push({
type: 'text',
content: text.slice(0, pos),
level: level
});
}
nodes.push({
type: 'link_open',
href: links[ln],
title: '',
level: level++
});
nodes.push({
type: 'text',
content: escapeHtml(links[ln]),
level: level
});
nodes.push({
type: 'link_close',
level: --level
});
text = text.slice(pos + links[ln].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));
}
}
});
rules.push(function single(t, state) {
var i, token, text,
tokens = state.tokens,
options = t.options;
for (i = tokens.length - 1; i >= 0; i--) {
token = tokens[i];
if (token.type === 'text') {
text = token.content;
if (text.indexOf('(') >= 0) {
if (options.copyright) {
text = text.replace(/\(c\)/gi, '©');
}
if (options.trademark) {
text = text.replace(/\(tm\)/gi, '™');
}
if (options.registered) {
text = text.replace(/\(r\)/gi, '®');
}
if (options.paragraph) {
text = text.replace(/\(p\)/gi, '§');
}
}
if (options.plusminus && text.indexOf('+-') >= 0) {
text = text.replace(/\+-/g, '±');
}
if (options.ellipsis && text.indexOf('..') >= 0) {
// .., ..., ....... -> …
// but ?..... & !..... -> ?.. & !..
text = text.replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..');
}
if (options.dupes &&
(text.indexOf('????') >= 0 ||
text.indexOf('!!!!') >= 0 ||
text.indexOf(',,') >= 0)) {
text = text.replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ',');
}
if (options.emDashes && text.indexOf('--') >= 0) {
text = text.replace(/(^|\s)--(\s|$)/mg, '$1—$2');
}
token.content = text;
}
}
});
rules.push(require('./rules_typographer/replace'));
function Typographer() {

7
support/specsplit.js

@ -57,12 +57,7 @@ function readFile(filename, encoding, callback) {
readFile(options.spec, 'utf8', function (error, input) {
var good = [], bad = [],
markdown = new Remarkable({
html: true,
breaks: false,
xhtml: true,
langPrefix: 'language-'
});
markdown = new Remarkable('commonmark');
if (error) {
if (error.code === 'ENOENT') {

4
test/remarkable.js

@ -11,9 +11,9 @@ var Remarked = require('../');
describe('Default', function () {
var md = new Remarked({
breaks: false,
langPrefix: '',
typographer: true
typographer: true,
linkify: true
});
utils.addSpecTests(path.join(__dirname, 'fixtures/remarkable'), md);

1
test/remarked.js

@ -14,7 +14,6 @@ describe('remarked', function () {
// Set options, to give output more close to remarked
md.set({
breaks: false,
langPrefix: 'lang-'
});

7
test/stmd.js

@ -10,12 +10,7 @@ var Remarked = require('../');
describe('stmd', function () {
var md = new Remarked({
html: true,
xhtml: true,
breaks: false,
langPrefix: 'language-'
});
var md = new Remarked('commonmark');
utils.addSpecTests(path.join(__dirname, 'fixtures/stmd/good.txt'), md);
});

Loading…
Cancel
Save