fulan-editor
Version:
An open source react editor based on draft-Js and ant design, good support HTML, markdown and Draft Raw format.
871 lines (723 loc) • 19.3 kB
JavaScript
/**
* Ported from:
* https://github.com/chjj/marked/blob/49b7eaca/lib/marked.js
* TODO:
* Use ES6 classes
* Add flow annotations
*/
/* eslint-disable no-spaced-func */
import {
TextNode,
ElementNode,
FragmentNode,
SELF_CLOSING,
} from 'synthetic-dom';
const hasOwnProperty = Object.prototype.hasOwnProperty;
const assign = Object.assign || function(obj) {
var i = 1;
for (; i < arguments.length; i++) {
var target = arguments[i];
for (var key in target) {
if (hasOwnProperty.call(target, key)) {
obj[key] = target[key];
}
}
}
return obj;
};
const noop = function() {};
noop.exec = noop;
var defaults = {
gfm: true,
breaks: false,
pedantic: false,
smartLists: false,
silent: false,
langPrefix: 'lang-',
renderer: new Renderer(),
xhtml: false,
};
/**
* Block-Level Grammar
*/
var block = {
newline: /^\n+/,
code: /^( {4}[^\n]+\n*)+/,
fences: noop,
hr: /^( *[-*_]){3,} *(?:\n+|$)/,
heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
nptable: noop,
lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,
list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|def))+)\n*/,
text: /^[^\n]+/,
};
block.bullet = /(?:[*+-]|\d+\.)/;
block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
block.item = replace(block.item, 'gm')
(/bull/g, block.bullet)
();
block.list = replace(block.list)
(/bull/g, block.bullet)
('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))')
('def', '\\n+(?=' + block.def.source + ')')
();
block.blockquote = replace(block.blockquote)
('def', block.def)
();
block.paragraph = replace(block.paragraph)
('hr', block.hr)
('heading', block.heading)
('lheading', block.lheading)
('blockquote', block.blockquote)
('def', block.def)
();
/**
* Normal Block Grammar
*/
block.normal = assign({}, block);
/**
* GFM Block Grammar
*/
block.gfm = assign({}, block.normal, {
fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,
paragraph: /^/,
heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/,
});
block.gfm.paragraph = replace(block.paragraph)
('(?!', '(?!'
+ block.gfm.fences.source.replace('\\1', '\\2') + '|'
+ block.list.source.replace('\\1', '\\3') + '|')
();
/**
* Block Lexer
*/
function Lexer(options) {
this.tokens = [];
this.tokens.links = {};
this.options = assign({}, options || defaults);
this.rules = block.normal;
if (this.options.gfm) {
this.rules = block.gfm;
}
}
/**
* Expose Block Rules
*/
Lexer.rules = block;
/**
* Static Lex Method
*/
Lexer.parse = function(src, options) {
var lexer = new Lexer(options);
return lexer.parse(src);
};
/**
* Preprocessing
*/
Lexer.prototype.parse = function(src) {
src = src
.replace(/\r\n|\r/g, '\n')
.replace(/\t/g, ' ')
.replace(/\u00a0/g, ' ')
.replace(/\u2424/g, '\n');
return this.token(src, true);
};
/**
* Lexing
*/
Lexer.prototype.token = function(src, top, bq) {
var next;
var loose;
var cap;
var bull;
var b;
var item;
var space;
var i;
var l;
src = src.replace(/^ +$/gm, '');
while (src) {
// newline
if ((cap = this.rules.newline.exec(src))) {
src = src.substring(cap[0].length);
if (cap[0].length > 1) {
this.tokens.push({
type: 'space',
});
}
}
// code
if ((cap = this.rules.code.exec(src))) {
src = src.substring(cap[0].length);
cap = cap[0].replace(/^ {4}/gm, '');
this.tokens.push({
type: 'code',
text: !this.options.pedantic ? cap.replace(/\n+$/, '') : cap,
});
continue;
}
// fences (gfm)
if ((cap = this.rules.fences.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'code',
lang: cap[2],
text: cap[3],
});
continue;
}
// heading
if ((cap = this.rules.heading.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'heading',
depth: cap[1].length,
text: cap[2],
});
continue;
}
// lheading
if ((cap = this.rules.lheading.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'heading',
depth: cap[2] === '=' ? 1 : 2,
text: cap[1],
});
continue;
}
// hr
if ((cap = this.rules.hr.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'hr',
});
continue;
}
// blockquote
if ((cap = this.rules.blockquote.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'blockquote_start',
});
cap = cap[0].replace(/^ *> ?/gm, '');
// Pass `top` to keep the current
// "toplevel" state. This is exactly
// how markdown.pl works.
this.token(cap, top, true);
this.tokens.push({
type: 'blockquote_end',
});
continue;
}
// list
if ((cap = this.rules.list.exec(src))) {
src = src.substring(cap[0].length);
bull = cap[2];
this.tokens.push({
type: 'list_start',
ordered: bull.length > 1,
});
// Get each top-level item.
cap = cap[0].match(this.rules.item);
next = false;
l = cap.length;
i = 0;
for (; i < l; i++) {
item = cap[i];
// Remove the list item's bullet
// so it is seen as the next token.
space = item.length;
item = item.replace(/^ *([*+-]|\d+\.) +/, '');
// Outdent whatever the
// list item contains. Hacky.
if (~item.indexOf('\n ')) {
space -= item.length;
item = !this.options.pedantic
? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
: item.replace(/^ {1,4}/gm, '');
}
// Determine whether the next list item belongs here.
// Backpedal if it does not belong in this list.
if (this.options.smartLists && i !== l - 1) {
b = block.bullet.exec(cap[i + 1])[0];
if (bull !== b && !(bull.length > 1 && b.length > 1)) {
src = cap.slice(i + 1).join('\n') + src;
i = l - 1;
}
}
// Determine whether item is loose or not.
// Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
// for discount behavior.
loose = next || /\n\n(?!\s*$)/.test(item);
if (i !== l - 1) {
next = item.charAt(item.length - 1) === '\n';
if (!loose) {
loose = next;
}
}
this.tokens.push({
type: loose ? 'loose_item_start' : 'list_item_start',
});
// Recurse.
this.token(item, false, bq);
this.tokens.push({
type: 'list_item_end',
});
}
this.tokens.push({
type: 'list_end',
});
continue;
}
// def
if ((!bq && top) && (cap = this.rules.def.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.links[cap[1].toLowerCase()] = {
href: cap[2],
title: cap[3],
};
continue;
}
// top-level paragraph
if (top && (cap = this.rules.paragraph.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'paragraph',
text: cap[1].charAt(cap[1].length - 1) === '\n'
? cap[1].slice(0, -1)
: cap[1],
});
continue;
}
// text
if ((cap = this.rules.text.exec(src))) {
// Top-level should never reach here.
src = src.substring(cap[0].length);
this.tokens.push({
type: 'text',
text: cap[0],
});
continue;
}
if (src) {
throw new
Error('Infinite loop on byte: ' + src.charCodeAt(0));
}
}
return this.tokens;
};
/**
* Inline-Level Grammar
*/
var inline = {
escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
link: /^!?\[(inside)\]\(href\)/,
reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
br: /^ {2,}\n(?!\s*$)/,
del: noop,
ins: noop,
text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/,
};
inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
inline._href = /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
inline.link = replace(inline.link)
('inside', inline._inside)
('href', inline._href)
();
inline.reflink = replace(inline.reflink)
('inside', inline._inside)
();
/**
* Normal Inline Grammar
*/
inline.normal = assign({}, inline);
/**
* Pedantic Inline Grammar
*/
inline.pedantic = assign({}, inline.normal, {
strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,
});
/**
* GFM Inline Grammar
*/
inline.gfm = assign({}, inline.normal, {
escape: replace(inline.escape)('])', '~|])')(),
del: /^~~(?=\S)([\s\S]*?\S)~~/,
ins: /^\+\+(?=\S)([\s\S]*?\S)\+\+/,
text: replace(inline.text)(']|', '~+]|')(),
});
/**
* GFM + Line Breaks Inline Grammar
*/
inline.breaks = assign({}, inline.gfm, {
br: replace(inline.br)('{2,}', '*')(),
text: replace(inline.gfm.text)('{2,}', '*')(),
});
/**
* Inline Lexer & Compiler
*/
function InlineLexer(links, options) {
this.options = assign({}, options || defaults);
this.links = links;
this.rules = inline.normal;
this.renderer = this.options.renderer || new Renderer();
this.renderer.options = this.options;
if (!this.links) {
throw new
Error('Tokens array requires a `links` property.');
}
if (this.options.gfm) {
if (this.options.breaks) {
this.rules = inline.breaks;
} else {
this.rules = inline.gfm;
}
} else if (this.options.pedantic) {
this.rules = inline.pedantic;
}
}
/**
* Expose Inline Rules
*/
InlineLexer.rules = inline;
/**
* Static Lexing/Compiling Method
*/
InlineLexer.parse = function(src, links, options) {
var inline = new InlineLexer(links, options);
return inline.parse(src);
};
/**
* Lexing/Compiling
*/
InlineLexer.prototype.parse = function(src) {
var out = new FragmentNode();
var link;
var cap;
while (src) {
// escape
if ((cap = this.rules.escape.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(new TextNode(cap[1]));
continue;
}
// link
if ((cap = this.rules.link.exec(src))) {
src = src.substring(cap[0].length);
this.inLink = true;
out.appendChild(this.outputLink(cap, {href: cap[2], title: cap[3]}));
this.inLink = false;
continue;
}
// reflink, nolink
if ((cap = this.rules.reflink.exec(src))
|| (cap = this.rules.nolink.exec(src))) {
src = src.substring(cap[0].length);
link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
link = this.links[link.toLowerCase()];
if (!link || !link.href) {
out.appendChild(new TextNode(cap[0].charAt(0)));
src = cap[0].substring(1) + src;
continue;
}
this.inLink = true;
out.appendChild(this.outputLink(cap, link));
this.inLink = false;
continue;
}
// strong
if ((cap = this.rules.strong.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(this.renderer.strong(this.parse(cap[2] || cap[1])));
continue;
}
// em
if ((cap = this.rules.em.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(this.renderer.em(this.parse(cap[2] || cap[1])));
continue;
}
// code
if ((cap = this.rules.code.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(this.renderer.codespan(cap[2]));
continue;
}
// br
if ((cap = this.rules.br.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(this.renderer.br());
continue;
}
// del (gfm)
if ((cap = this.rules.del.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(this.renderer.del(this.parse(cap[1])));
continue;
}
// ins (gfm extended)
if ((cap = this.rules.ins.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(this.renderer.ins(this.parse(cap[1])));
continue;
}
// text
if ((cap = this.rules.text.exec(src))) {
src = src.substring(cap[0].length);
out.appendChild(this.renderer.text(new TextNode(cap[0])));
continue;
}
if (src) {
throw new
Error('Infinite loop on byte: ' + src.charCodeAt(0));
}
}
return out;
};
/**
* Compile Link
*/
InlineLexer.prototype.outputLink = function(cap, link) {
var href = link.href;
var title = link.title;
return cap[0].charAt(0) !== '!'
? this.renderer.link(href, title, this.parse(cap[1]))
: this.renderer.image(href, title, cap[1]);
};
/**
* Renderer
*/
function Renderer(options) {
this.options = options || {};
}
Renderer.prototype.code = function(childNode, lang) {
var attributes = [];
if (lang) {
attributes.push(['class', this.options.langPrefix + lang]);
}
var codeNode = new ElementNode('code', attributes, [childNode]);
return new ElementNode('pre', [], [codeNode]);
};
Renderer.prototype.blockquote = function(childNode) {
return new ElementNode('blockquote', [], [childNode]);
};
Renderer.prototype.heading = function(childNode, level) {
return new ElementNode('h' + level, [], [childNode]);
};
Renderer.prototype.hr = function() {
return new ElementNode('hr', [], SELF_CLOSING);
};
Renderer.prototype.list = function(childNode, isOrdered) {
return new ElementNode(isOrdered ? 'ol' : 'ul', [], [childNode]);
};
Renderer.prototype.listitem = function(childNode) {
return new ElementNode('li', [], [childNode]);
};
Renderer.prototype.paragraph = function(childNode) {
return new ElementNode('p', [], [childNode]);
};
// span level renderer
Renderer.prototype.strong = function(childNode) {
return new ElementNode('strong', [], [childNode]);
};
Renderer.prototype.em = function(childNode) {
return new ElementNode('em', [], [childNode]);
};
Renderer.prototype.codespan = function(text) {
return new ElementNode('code', [], [new TextNode(text)]);
};
Renderer.prototype.br = function() {
return new ElementNode('br', [], SELF_CLOSING);
};
Renderer.prototype.del = function(childNode) {
return new ElementNode('del', [], [childNode]);
};
Renderer.prototype.ins = function(childNode) {
return new ElementNode('ins', [], [childNode]);
};
Renderer.prototype.link = function(href, title, childNode) {
var attributes = [
['href', href],
];
if (title) {
attributes.push(['title', title]);
}
return new ElementNode('a', attributes, [childNode]);
};
Renderer.prototype.image = function(href, title, alt) {
var attributes = [
['src', href],
];
if (title) {
attributes.push(['title', title]);
}
if (alt) {
attributes.push(['alt', alt]);
}
return new ElementNode('img', attributes, SELF_CLOSING);
};
Renderer.prototype.text = function(childNode) {
return childNode;
};
/**
* Parsing & Compiling
*/
function Parser(options) {
this.tokens = [];
this.token = null;
this.options = assign({}, options || defaults);
this.options.renderer = this.options.renderer || new Renderer();
this.renderer = this.options.renderer;
this.renderer.options = this.options;
}
/**
* Static Parse Method
*/
Parser.parse = function(src, options, renderer) {
var parser = new Parser(options, renderer);
return parser.parse(src);
};
/**
* Parse Loop
*/
Parser.prototype.parse = function(src) {
this.inline = new InlineLexer(src.links, this.options, this.renderer);
this.tokens = src.slice().reverse();
var out = new FragmentNode();
while (this.next()) {
out.appendChild(this.tok());
}
return out;
};
/**
* Next Token
*/
Parser.prototype.next = function() {
return (this.token = this.tokens.pop());
};
/**
* Preview Next Token
*/
Parser.prototype.peek = function() {
return this.tokens[this.tokens.length - 1] || 0;
};
/**
* Parse Text Tokens
*/
Parser.prototype.parseText = function() {
var body = this.token.text;
while (this.peek().type === 'text') {
body += '\n' + this.next().text;
}
return this.inline.parse(body);
};
/**
* Parse Current Token
*/
Parser.prototype.tok = function() {
switch (this.token.type) {
case 'space': {
return new TextNode('');
}
case 'hr': {
return this.renderer.hr();
}
case 'heading': {
return this.renderer.heading(
this.inline.parse(this.token.text),
this.token.depth
);
}
case 'code': {
return this.renderer.code(
this.token.text,
this.token.lang
);
}
case 'blockquote_start': {
let body = new FragmentNode();
while (this.next().type !== 'blockquote_end') {
body.appendChild(this.tok());
}
return this.renderer.blockquote(body);
}
case 'list_start': {
let body = new FragmentNode();
var ordered = this.token.ordered;
while (this.next().type !== 'list_end') {
body.appendChild(this.tok());
}
return this.renderer.list(body, ordered);
}
case 'list_item_start': {
let body = new FragmentNode();
while (this.next().type !== 'list_item_end') {
body.appendChild(this.token.type === 'text'
? this.parseText()
: this.tok());
}
return this.renderer.listitem(body);
}
case 'loose_item_start': {
let body = new FragmentNode();
while (this.next().type !== 'list_item_end') {
body.appendChild(this.tok());
}
return this.renderer.listitem(body);
}
case 'paragraph': {
return this.renderer.paragraph(this.inline.parse(this.token.text));
}
case 'text': {
return this.renderer.paragraph(this.parseText());
}
}
};
/**
* Helpers
*/
function replace(regex, options) {
regex = regex.source;
options = options || '';
return function self(name, val) {
if (!name) {
return new RegExp(regex, options);
}
val = val.source || val;
val = val.replace(/(^|[^\[])\^/g, '$1');
regex = regex.replace(name, val);
return self;
};
}
const MarkdownParser = {
parse(src, options) {
options = assign({}, defaults, options);
try {
var fragment = Parser.parse(Lexer.parse(src, options), options);
} catch (e) {
if (options.silent) {
fragment = new FragmentNode([
new ElementNode('p', [], [new TextNode('An error occured:')]),
new ElementNode('pre', [], [new TextNode(e.message)]),
]);
} else {
throw e;
}
}
if (options.getAST) {
return new ElementNode('body', [], [fragment]);
} else {
return fragment.toString(this.options.xhtml);
}
},
};
export default MarkdownParser;