Browse Source

Added a core chain

pull/14/head
Alex Kocharin 11 years ago
parent
commit
d39417e100
  1. 59
      lib/index.js
  2. 42
      lib/linkifier.js
  3. 11
      lib/parser_inline.js
  4. 18
      lib/renderer.js
  5. 11
      lib/rules_block/paragraph.js
  6. 6
      lib/rules_core/block.js
  7. 13
      lib/rules_core/inline.js
  8. 128
      lib/rules_core/linkify.js
  9. 28
      lib/rules_core/references.js
  10. 14
      lib/rules_core/typographer.js
  11. 120
      lib/rules_text/linkify.js
  12. 6
      lib/rules_text/replace.js
  13. 4
      lib/rules_text/smartquotes.js
  14. 4
      lib/typographer.js

59
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 :)

42
lib/linkifier.js

@ -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;

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

18
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 ? '' : '<p>';
};
rules.paragraph_close = function (tokens, idx /*, options*/) {
return (tokens[idx].tight ? '' : '</p>') + getBreak(tokens, idx);
var addBreak = !(tokens[idx].tight && idx && tokens[idx - 1].type === 'inline' && !tokens[idx - 1].content);
return (tokens[idx].tight ? '' : '</p>') + (addBreak ? getBreak(tokens, idx) : '');
};

11
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({

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

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

128
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 /^<a[>\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));
}
}
}
};

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

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

120
lib/rules_text/linkify.js

@ -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 /^<a[>\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));
}
}
};

6
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];

4
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;

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

Loading…
Cancel
Save