Merge branch 'master' into master

This commit is contained in:
Tony Brix 2020-04-09 20:09:04 -05:00 committed by GitHub
commit c735fa58ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 4118 additions and 1870 deletions

View File

@ -17,6 +17,7 @@
"one-var": "off",
"no-control-regex": "off",
"no-prototype-builtins": "off",
"no-extra-semi": "error",
"prefer-const": "error",
"no-var": "error"

View File

@ -4,7 +4,7 @@ To champion the single-responsibility and open/closed principles, we have tried
<h2 id="renderer">The renderer</h2>
The renderer is...
The renderer defines the output of the parser.
**Example:** Overriding default heading token by adding an embedded anchor tag like on GitHub.
@ -29,7 +29,7 @@ renderer.heading = function (text, level) {
};
// Run marked
console.log(marked('# heading+', { renderer: renderer }));
console.log(marked('# heading+', { renderer }));
```
**Output:**
@ -91,12 +91,11 @@ slugger.slug('foo-1') // foo-1-2
<h2 id="lexer">The lexer</h2>
The lexer is...
The lexer turns a markdown string into tokens.
<h2 id="parser">The parser</h2>
The parser is...
The parser takes tokens as input and calls the renderer functions.
***
@ -105,30 +104,46 @@ The parser is...
You also have direct access to the lexer and parser if you so desire.
``` js
const tokens = marked.lexer(text, options);
const tokens = marked.lexer(markdown, options);
console.log(marked.parser(tokens, options));
```
``` js
const lexer = new marked.Lexer(options);
const tokens = lexer.lex(text);
const tokens = lexer.lex(markdown);
console.log(tokens);
console.log(lexer.rules);
console.log(lexer.rules.block); // block level rules
console.log(lexer.rules.inline); // inline level rules
```
``` bash
$ node
> require('marked').lexer('> i am using marked.')
[ { type: 'blockquote_start' },
{ type: 'paragraph',
text: 'i am using marked.' },
{ type: 'blockquote_end' },
links: {} ]
> require('marked').lexer('> I am using marked.')
[
{
type: "blockquote",
raw: "> I am using marked.",
tokens: [
{
type: "paragraph",
raw: "I am using marked.",
text: "I am using marked.",
tokens: [
{
type: "text",
raw: "I am using marked.",
text: "I am using marked."
}
]
}
]
},
links: {}
]
```
The Lexers build an array of tokens, which will be passed to their respective
Parsers. The Parsers process each token in the token arrays,
which are removed from the array of tokens:
The Lexer builds an array of tokens, which will be passed to the Parser.
The Parser processes each token in the token array:
``` js
const marked = require('marked');
@ -146,18 +161,60 @@ console.log(tokens);
const html = marked.parser(tokens);
console.log(html);
console.log(tokens);
```
``` bash
[ { type: 'heading', depth: 1, text: 'heading' },
{ type: 'paragraph', text: ' [link][1]' },
{ type: 'space' },
links: { '1': { href: '#heading', title: 'heading' } } ]
[
{
type: "heading",
raw: " # heading\n\n",
depth: 1,
text: "heading",
tokens: [
{
type: "text",
raw: "heading",
text: "heading"
}
]
},
{
type: "paragraph",
raw: " [link][1]",
text: " [link][1]",
tokens: [
{
type: "text",
raw: " ",
text: " "
},
{
type: "link",
raw: "[link][1]",
text: "link",
href: "#heading",
title: "heading",
tokens: [
{
type: "text",
raw: "link",
text: "link"
}
]
}
]
},
{
type: "space",
raw: "\n\n"
},
links: {
"1": {
href: "#heading",
title: "heading"
}
}
]
<h1 id="heading">heading</h1>
<p> <a href="#heading" title="heading">link</a></p>
[ links: { '1': { href: '#heading', title: 'heading' } } ]
```

View File

@ -183,7 +183,7 @@ function handleIframeLoad() {
function handleInput() {
inputDirty = true;
};
}
function handleVersionChange() {
if ($markedVerElem.value === 'commit' || $markedVerElem.value === 'pr') {
@ -256,7 +256,7 @@ function handleChange(panes, visiblePane) {
}
}
return active;
};
}
function addCommitVersion(value, text, commit) {
if (markedVersions[value]) {
@ -331,13 +331,13 @@ function jsonString(input) {
.replace(/[\\"']/g, '\\$&')
.replace(/\u0000/g, '\\0');
return '"' + output + '"';
};
}
function getScrollSize() {
var e = $activeOutputElem;
return e.scrollHeight - e.clientHeight;
};
}
function getScrollPercent() {
var size = getScrollSize();
@ -347,11 +347,11 @@ function getScrollPercent() {
}
return $activeOutputElem.scrollTop / size;
};
}
function setScrollPercent(percent) {
$activeOutputElem.scrollTop = percent * getScrollSize();
};
}
function updateLink() {
var outputType = '';
@ -446,7 +446,7 @@ function checkForChanges() {
}
}
checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
};
}
function setResponseTime(ms) {
var amount = ms;

View File

@ -1,4 +1,5 @@
/* globals marked, unfetch, ES6Promise, Promise */ // eslint-disable-line no-redeclare
if (!self.Promise) {
self.importScripts('https://cdn.jsdelivr.net/npm/es6-promise/dist/es6-promise.js');
self.Promise = ES6Promise;
@ -48,28 +49,44 @@ function parse(e) {
case 'parse':
var startTime = new Date();
var lexed = marked.lexer(e.data.markdown, e.data.options);
var lexedList = [];
for (var i = 0; i < lexed.length; i++) {
var lexedLine = [];
for (var j in lexed[i]) {
lexedLine.push(j + ':' + jsonString(lexed[i][j]));
}
lexedList.push('{' + lexedLine.join(', ') + '}');
}
var lexedList = getLexedList(lexed);
var parsed = marked.parser(lexed, e.data.options);
var endTime = new Date();
// setTimeout(function () {
postMessage({
task: e.data.task,
lexed: lexedList.join('\n'),
lexed: lexedList,
parsed: parsed,
time: endTime - startTime
});
// }, 10000);
break;
}
}
function getLexedList(lexed, level) {
level = level || 0;
var lexedList = [];
for (var i = 0; i < lexed.length; i++) {
var lexedLine = [];
for (var j in lexed[i]) {
if (j === 'tokens' || j === 'items') {
lexedLine.push(j + ': [\n' + getLexedList(lexed[i][j], level + 1) + '\n]');
} else {
lexedLine.push(j + ':' + jsonString(lexed[i][j]));
}
}
lexedList.push(stringRepeat(' ', 2 * level) + '{' + lexedLine.join(', ') + '}');
}
return lexedList.join('\n');
}
function stringRepeat(char, times) {
var s = '';
for (var i = 0; i < times; i++) {
s += char;
}
return s;
}
function jsonString(input) {
var output = (input + '')
.replace(/\n/g, '\\n')
@ -79,7 +96,7 @@ function jsonString(input) {
.replace(/[\\"']/g, '\\$&')
.replace(/\u0000/g, '\\0');
return '"' + output + '"';
};
}
function loadVersion(ver) {
var promise;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2
marked.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,293 +0,0 @@
const Renderer = require('./Renderer.js');
const { defaults } = require('./defaults.js');
const { inline } = require('./rules.js');
const {
findClosingBracket,
escape
} = require('./helpers.js');
/**
* Inline Lexer & Compiler
*/
module.exports = class InlineLexer {
constructor(links, options) {
this.options = options || defaults;
this.links = links;
this.rules = inline.normal;
this.options.renderer = this.options.renderer || new Renderer();
this.renderer = this.options.renderer;
this.renderer.options = this.options;
if (!this.links) {
throw new Error('Tokens array requires a `links` property.');
}
if (this.options.pedantic) {
this.rules = inline.pedantic;
} else if (this.options.gfm) {
if (this.options.breaks) {
this.rules = inline.breaks;
} else {
this.rules = inline.gfm;
}
}
}
/**
* Expose Inline Rules
*/
static get rules() {
return inline;
}
/**
* Static Lexing/Compiling Method
*/
static output(src, links, options) {
const inline = new InlineLexer(links, options);
return inline.output(src);
}
/**
* Lexing/Compiling
*/
output(src) {
let out = '',
link,
text,
href,
title,
cap,
prevCapZero;
while (src) {
// escape
if (cap = this.rules.escape.exec(src)) {
src = src.substring(cap[0].length);
out += escape(cap[1]);
continue;
}
// tag
if (cap = this.rules.tag.exec(src)) {
if (!this.inLink && /^<a /i.test(cap[0])) {
this.inLink = true;
} else if (this.inLink && /^<\/a>/i.test(cap[0])) {
this.inLink = false;
}
if (!this.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) {
this.inRawBlock = true;
} else if (this.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) {
this.inRawBlock = false;
}
src = src.substring(cap[0].length);
out += this.renderer.html(this.options.sanitize
? (this.options.sanitizer
? this.options.sanitizer(cap[0])
: escape(cap[0]))
: cap[0]);
continue;
}
// link
if (cap = this.rules.link.exec(src)) {
const lastParenIndex = findClosingBracket(cap[2], '()');
if (lastParenIndex > -1) {
const start = cap[0].indexOf('!') === 0 ? 5 : 4;
const linkLen = start + cap[1].length + lastParenIndex;
cap[2] = cap[2].substring(0, lastParenIndex);
cap[0] = cap[0].substring(0, linkLen).trim();
cap[3] = '';
}
src = src.substring(cap[0].length);
this.inLink = true;
href = cap[2];
if (this.options.pedantic) {
link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href);
if (link) {
href = link[1];
title = link[3];
} else {
title = '';
}
} else {
title = cap[3] ? cap[3].slice(1, -1) : '';
}
href = href.trim().replace(/^<([\s\S]*)>$/, '$1');
out += this.outputLink(cap, {
href: InlineLexer.escapes(href),
title: InlineLexer.escapes(title)
});
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 += cap[0].charAt(0);
src = cap[0].substring(1) + src;
continue;
}
this.inLink = true;
out += this.outputLink(cap, link);
this.inLink = false;
continue;
}
// strong
if (cap = this.rules.strong.exec(src)) {
src = src.substring(cap[0].length);
out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1]));
continue;
}
// em
if (cap = this.rules.em.exec(src)) {
src = src.substring(cap[0].length);
out += this.renderer.em(this.output(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1]));
continue;
}
// code
if (cap = this.rules.code.exec(src)) {
src = src.substring(cap[0].length);
out += this.renderer.codespan(escape(cap[2].trim(), true));
continue;
}
// br
if (cap = this.rules.br.exec(src)) {
src = src.substring(cap[0].length);
out += this.renderer.br();
continue;
}
// del (gfm)
if (cap = this.rules.del.exec(src)) {
src = src.substring(cap[0].length);
out += this.renderer.del(this.output(cap[1]));
continue;
}
// autolink
if (cap = this.rules.autolink.exec(src)) {
src = src.substring(cap[0].length);
if (cap[2] === '@') {
text = escape(this.mangle(cap[1]));
href = 'mailto:' + text;
} else {
text = escape(cap[1]);
href = text;
}
out += this.renderer.link(href, null, text);
continue;
}
// url (gfm)
if (!this.inLink && (cap = this.rules.url.exec(src))) {
if (cap[2] === '@') {
text = escape(cap[0]);
href = 'mailto:' + text;
} else {
// do extended autolink path validation
do {
prevCapZero = cap[0];
cap[0] = this.rules._backpedal.exec(cap[0])[0];
} while (prevCapZero !== cap[0]);
text = escape(cap[0]);
if (cap[1] === 'www.') {
href = 'http://' + text;
} else {
href = text;
}
}
src = src.substring(cap[0].length);
out += this.renderer.link(href, null, text);
continue;
}
// text
if (cap = this.rules.text.exec(src)) {
src = src.substring(cap[0].length);
if (this.inRawBlock) {
out += this.renderer.text(this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]);
} else {
out += this.renderer.text(escape(this.smartypants(cap[0])));
}
continue;
}
if (src) {
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
}
}
return out;
}
static escapes(text) {
return text ? text.replace(InlineLexer.rules._escapes, '$1') : text;
}
/**
* Compile Link
*/
outputLink(cap, link) {
const href = link.href,
title = link.title ? escape(link.title) : null;
return cap[0].charAt(0) !== '!'
? this.renderer.link(href, title, this.output(cap[1]))
: this.renderer.image(href, title, escape(cap[1]));
}
/**
* Smartypants Transformations
*/
smartypants(text) {
if (!this.options.smartypants) return text;
return text
// em-dashes
.replace(/---/g, '\u2014')
// en-dashes
.replace(/--/g, '\u2013')
// opening singles
.replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
// closing singles & apostrophes
.replace(/'/g, '\u2019')
// opening doubles
.replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
// closing doubles
.replace(/"/g, '\u201d')
// ellipses
.replace(/\.{3}/g, '\u2026');
}
/**
* Mangle Links
*/
mangle(text) {
if (!this.options.mangle) return text;
const l = text.length;
let out = '',
i = 0,
ch;
for (; i < l; i++) {
ch = text.charCodeAt(i);
if (Math.random() > 0.5) {
ch = 'x' + ch.toString(16);
}
out += '&#' + ch + ';';
}
return out;
}
};

View File

@ -1,9 +1,10 @@
const { defaults } = require('./defaults.js');
const { block } = require('./rules.js');
const { block, inline } = require('./rules.js');
const {
rtrim,
splitCells,
escape
escape,
findClosingBracket
} = require('./helpers.js');
/**
@ -14,12 +15,21 @@ module.exports = class Lexer {
this.tokens = [];
this.tokens.links = Object.create(null);
this.options = options || defaults;
this.rules = block.normal;
this.rules = {
block: block.normal,
inline: inline.normal
};
if (this.options.pedantic) {
this.rules = block.pedantic;
this.rules.block = block.pedantic;
this.rules.inline = inline.pedantic;
} else if (this.options.gfm) {
this.rules = block.gfm;
this.rules.block = block.gfm;
if (this.options.breaks) {
this.rules.inline = inline.breaks;
} else {
this.rules.inline = inline.gfm;
}
}
}
@ -27,7 +37,10 @@ module.exports = class Lexer {
* Expose Block Rules
*/
static get rules() {
return block;
return {
block,
inline
};
}
/**
@ -36,7 +49,7 @@ module.exports = class Lexer {
static lex(src, options) {
const lexer = new Lexer(options);
return lexer.lex(src);
};
}
/**
* Preprocessing
@ -46,13 +59,17 @@ module.exports = class Lexer {
.replace(/\r\n|\r/g, '\n')
.replace(/\t/g, ' ');
return this.token(src, true);
};
this.blockTokens(src, this.tokens);
this.inline(this.tokens);
return this.tokens;
}
/**
* Lexing
*/
token(src, top) {
blockTokens(src, tokens, top = true) {
src = src.replace(/^ +$/gm, '');
let next,
loose,
@ -60,39 +77,45 @@ module.exports = class Lexer {
bull,
b,
item,
listStart,
listItems,
t,
list,
space,
i,
tag,
l,
isordered,
istask,
ischecked;
ischecked,
lastToken,
addBack,
raw;
while (src) {
// newline
if (cap = this.rules.newline.exec(src)) {
if (cap = this.rules.block.newline.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
if (cap[0].length > 1) {
this.tokens.push({
type: 'space'
tokens.push({
type: 'space',
raw
});
}
}
// code
if (cap = this.rules.code.exec(src)) {
const lastToken = this.tokens[this.tokens.length - 1];
if (cap = this.rules.block.code.exec(src)) {
lastToken = tokens[tokens.length - 1];
src = src.substring(cap[0].length);
raw = cap[0];
// An indented code block cannot interrupt a paragraph.
if (lastToken && lastToken.type === 'paragraph') {
lastToken.text += '\n' + cap[0].trimRight();
lastToken.raw += '\n' + raw;
} else {
cap = cap[0].replace(/^ {4}/gm, '');
this.tokens.push({
tokens.push({
type: 'code',
raw,
codeBlockStyle: 'indented',
text: !this.options.pedantic
? rtrim(cap, '\n')
@ -103,10 +126,12 @@ module.exports = class Lexer {
}
// fences
if (cap = this.rules.fences.exec(src)) {
if (cap = this.rules.block.fences.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
raw = cap[0];
tokens.push({
type: 'code',
raw,
lang: cap[2] ? cap[2].trim() : cap[2],
text: cap[3] || ''
});
@ -114,10 +139,12 @@ module.exports = class Lexer {
}
// heading
if (cap = this.rules.heading.exec(src)) {
if (cap = this.rules.block.heading.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
raw = cap[0];
tokens.push({
type: 'heading',
raw,
depth: cap[1].length,
text: cap[2]
});
@ -125,7 +152,7 @@ module.exports = class Lexer {
}
// table no leading pipe (gfm)
if (cap = this.rules.nptable.exec(src)) {
if (cap = this.rules.block.nptable.exec(src)) {
item = {
type: 'table',
header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
@ -135,8 +162,11 @@ module.exports = class Lexer {
if (item.header.length === item.align.length) {
src = src.substring(cap[0].length);
raw = cap[0];
item.raw = raw;
for (i = 0; i < item.align.length; i++) {
l = item.align.length;
for (i = 0; i < l; i++) {
if (/^ *-+: *$/.test(item.align[i])) {
item.align[i] = 'right';
} else if (/^ *:-+: *$/.test(item.align[i])) {
@ -148,72 +178,71 @@ module.exports = class Lexer {
}
}
for (i = 0; i < item.cells.length; i++) {
l = item.cells.length;
for (i = 0; i < l; i++) {
item.cells[i] = splitCells(item.cells[i], item.header.length);
}
this.tokens.push(item);
tokens.push(item);
continue;
}
}
// hr
if (cap = this.rules.hr.exec(src)) {
if (cap = this.rules.block.hr.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'hr'
raw = cap[0];
tokens.push({
type: 'hr',
raw
});
continue;
}
// blockquote
if (cap = this.rules.blockquote.exec(src)) {
if (cap = this.rules.block.blockquote.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'blockquote_start'
});
raw = cap[0];
cap = cap[0].replace(/^ *> ?/gm, '');
// Pass `top` to keep the current
// "toplevel" state. This is exactly
// how markdown.pl works.
this.token(cap, top);
this.tokens.push({
type: 'blockquote_end'
tokens.push({
type: 'blockquote',
raw,
tokens: this.blockTokens(cap, [], top)
});
continue;
}
// list
if (cap = this.rules.list.exec(src)) {
if (cap = this.rules.block.list.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
bull = cap[2];
isordered = bull.length > 1;
listStart = {
type: 'list_start',
list = {
type: 'list',
raw,
ordered: isordered,
start: isordered ? +bull : '',
loose: false
loose: false,
items: []
};
this.tokens.push(listStart);
tokens.push(list);
// Get each top-level item.
cap = cap[0].match(this.rules.item);
cap = cap[0].match(this.rules.block.item);
listItems = [];
next = false;
l = cap.length;
i = 0;
for (; i < l; i++) {
l = cap.length;
for (i = 0; i < l; i++) {
item = cap[i];
raw = item.trim();
// Remove the list item's bullet
// so it is seen as the next token.
@ -235,7 +264,9 @@ module.exports = class Lexer {
b = block.bullet.exec(cap[i + 1])[0];
if (bull.length > 1 ? b.length === 1
: (b.length > 1 || (this.options.smartLists && b !== bull))) {
src = cap.slice(i + 1).join('\n') + src;
addBack = cap.slice(i + 1).join('\n');
src = addBack + src;
list.raw = list.raw.substring(list.raw.length - addBack.length);
i = l - 1;
}
}
@ -250,7 +281,7 @@ module.exports = class Lexer {
}
if (loose) {
listStart.loose = true;
list.loose = true;
}
// Check for task list items
@ -261,46 +292,27 @@ module.exports = class Lexer {
item = item.replace(/^\[[ xX]\] +/, '');
}
t = {
type: 'list_item_start',
list.items.push({
raw,
task: istask,
checked: ischecked,
loose: loose
};
listItems.push(t);
this.tokens.push(t);
// Recurse.
this.token(item, false);
this.tokens.push({
type: 'list_item_end'
loose: loose,
tokens: this.blockTokens(item, [], false)
});
}
if (listStart.loose) {
l = listItems.length;
i = 0;
for (; i < l; i++) {
listItems[i].loose = true;
}
}
this.tokens.push({
type: 'list_end'
});
continue;
}
// html
if (cap = this.rules.html.exec(src)) {
if (cap = this.rules.block.html.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
raw = cap[0];
tokens.push({
type: this.options.sanitize
? 'paragraph'
: 'html',
raw,
pre: !this.options.sanitizer
&& (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]
@ -309,7 +321,7 @@ module.exports = class Lexer {
}
// def
if (top && (cap = this.rules.def.exec(src))) {
if (top && (cap = this.rules.block.def.exec(src))) {
src = src.substring(cap[0].length);
if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1);
tag = cap[1].toLowerCase().replace(/\s+/g, ' ');
@ -323,7 +335,7 @@ module.exports = class Lexer {
}
// table (gfm)
if (cap = this.rules.table.exec(src)) {
if (cap = this.rules.block.table.exec(src)) {
item = {
type: 'table',
header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
@ -333,8 +345,10 @@ module.exports = class Lexer {
if (item.header.length === item.align.length) {
src = src.substring(cap[0].length);
item.raw = cap[0];
for (i = 0; i < item.align.length; i++) {
l = item.align.length;
for (i = 0; i < l; i++) {
if (/^ *-+: *$/.test(item.align[i])) {
item.align[i] = 'right';
} else if (/^ *:-+: *$/.test(item.align[i])) {
@ -346,23 +360,26 @@ module.exports = class Lexer {
}
}
for (i = 0; i < item.cells.length; i++) {
l = item.cells.length;
for (i = 0; i < l; i++) {
item.cells[i] = splitCells(
item.cells[i].replace(/^ *\| *| *\| *$/g, ''),
item.header.length);
}
this.tokens.push(item);
tokens.push(item);
continue;
}
}
// lheading
if (cap = this.rules.lheading.exec(src)) {
if (cap = this.rules.block.lheading.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
raw = cap[0];
tokens.push({
type: 'heading',
raw,
depth: cap[2].charAt(0) === '=' ? 1 : 2,
text: cap[1]
});
@ -370,10 +387,12 @@ module.exports = class Lexer {
}
// top-level paragraph
if (top && (cap = this.rules.paragraph.exec(src))) {
if (top && (cap = this.rules.block.paragraph.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
raw = cap[0];
tokens.push({
type: 'paragraph',
raw,
text: cap[1].charAt(cap[1].length - 1) === '\n'
? cap[1].slice(0, -1)
: cap[1]
@ -382,21 +401,463 @@ module.exports = class Lexer {
}
// text
if (cap = this.rules.text.exec(src)) {
if (cap = this.rules.block.text.exec(src)) {
// Top-level should never reach here.
src = src.substring(cap[0].length);
this.tokens.push({
raw = cap[0];
tokens.push({
type: 'text',
raw,
text: cap[0]
});
continue;
}
if (src) {
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);
if (this.options.silent) {
console.error(errMsg);
} else {
throw new Error(errMsg);
}
}
}
return this.tokens;
};
return tokens;
}
inline(tokens) {
let i,
j,
k,
l2,
row,
token;
const l = tokens.length;
for (i = 0; i < l; i++) {
token = tokens[i];
switch (token.type) {
case 'paragraph':
case 'text':
case 'heading': {
token.tokens = [];
this.inlineTokens(token.text, token.tokens);
break;
}
case 'table': {
token.tokens = {
header: [],
cells: []
};
// header
l2 = token.header.length;
for (j = 0; j < l2; j++) {
token.tokens.header[j] = [];
this.inlineTokens(token.header[j], token.tokens.header[j]);
}
// cells
l2 = token.cells.length;
for (j = 0; j < l2; j++) {
row = token.cells[j];
token.tokens.cells[j] = [];
for (k = 0; k < row.length; k++) {
token.tokens.cells[j][k] = [];
this.inlineTokens(row[k], token.tokens.cells[j][k]);
}
}
break;
}
case 'blockquote': {
this.inline(token.tokens);
break;
}
case 'list': {
l2 = token.items.length;
for (j = 0; j < l2; j++) {
this.inline(token.items[j].tokens);
}
break;
}
default: {
// do nothing
}
}
}
return tokens;
}
/**
* Lexing/Compiling
*/
inlineTokens(src, tokens) {
let out = '',
link,
text,
newTokens,
href,
title,
cap,
prevCapZero,
lastParenIndex,
start,
linkLen,
raw;
while (src) {
// escape
if (cap = this.rules.inline.escape.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
text = escape(cap[1]);
out += text;
tokens.push({
type: 'escape',
raw,
text
});
continue;
}
// tag
if (cap = this.rules.inline.tag.exec(src)) {
if (!this.inLink && /^<a /i.test(cap[0])) {
this.inLink = true;
} else if (this.inLink && /^<\/a>/i.test(cap[0])) {
this.inLink = false;
}
if (!this.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) {
this.inRawBlock = true;
} else if (this.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) {
this.inRawBlock = false;
}
src = src.substring(cap[0].length);
raw = cap[0];
text = this.options.sanitize
? (this.options.sanitizer
? this.options.sanitizer(cap[0])
: escape(cap[0]))
: cap[0];
tokens.push({
type: this.options.sanitize
? 'text'
: 'html',
raw,
text
});
out += text;
continue;
}
// link
if (cap = this.rules.inline.link.exec(src)) {
lastParenIndex = findClosingBracket(cap[2], '()');
if (lastParenIndex > -1) {
start = cap[0].indexOf('!') === 0 ? 5 : 4;
linkLen = start + cap[1].length + lastParenIndex;
cap[2] = cap[2].substring(0, lastParenIndex);
cap[0] = cap[0].substring(0, linkLen).trim();
cap[3] = '';
}
src = src.substring(cap[0].length);
raw = cap[0];
this.inLink = true;
href = cap[2];
if (this.options.pedantic) {
link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href);
if (link) {
href = link[1];
title = link[3];
} else {
title = '';
}
} else {
title = cap[3] ? cap[3].slice(1, -1) : '';
}
href = href.trim().replace(/^<([\s\S]*)>$/, '$1');
out += this.outputLink(cap, {
href: this.escapes(href),
title: this.escapes(title)
}, tokens, raw);
this.inLink = false;
continue;
}
// reflink, nolink
if ((cap = this.rules.inline.reflink.exec(src))
|| (cap = this.rules.inline.nolink.exec(src))) {
src = src.substring(cap[0].length);
raw = cap[0];
link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
link = this.tokens.links[link.toLowerCase()];
if (!link || !link.href) {
text = cap[0].charAt(0);
out += text;
tokens.push({
type: 'text',
raw: text,
text
});
src = cap[0].substring(1) + src;
continue;
}
this.inLink = true;
out += this.outputLink(cap, link, tokens, raw);
this.inLink = false;
continue;
}
// strong
if (cap = this.rules.inline.strong.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
newTokens = tokens ? [] : null;
text = this.inlineTokens(cap[4] || cap[3] || cap[2] || cap[1], newTokens);
tokens.push({
type: 'strong',
raw,
text,
tokens: newTokens
});
out += text;
continue;
}
// em
if (cap = this.rules.inline.em.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
newTokens = tokens ? [] : null;
text = this.inlineTokens(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1], newTokens);
tokens.push({
type: 'em',
raw,
text,
tokens: newTokens
});
out += text;
continue;
}
// code
if (cap = this.rules.inline.code.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
text = escape(cap[2].trim(), true);
tokens.push({
type: 'codespan',
raw,
text
});
out += text;
continue;
}
// br
if (cap = this.rules.inline.br.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
tokens.push({
type: 'br',
raw
});
out += '\n';
continue;
}
// del (gfm)
if (cap = this.rules.inline.del.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
newTokens = tokens ? [] : null;
text = this.inlineTokens(cap[1], newTokens);
tokens.push({
type: 'del',
raw,
text,
tokens: newTokens
});
out += text;
continue;
}
// autolink
if (cap = this.rules.inline.autolink.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
if (cap[2] === '@') {
text = escape(this.options.mangle ? this.mangle(cap[1]) : cap[1]);
href = 'mailto:' + text;
} else {
text = escape(cap[1]);
href = text;
}
tokens.push({
type: 'link',
raw,
text,
href,
tokens: [
{
type: 'text',
raw: text,
text
}
]
});
out += text;
continue;
}
// url (gfm)
if (!this.inLink && (cap = this.rules.inline.url.exec(src))) {
if (cap[2] === '@') {
text = escape(this.options.mangle ? this.mangle(cap[0]) : cap[0]);
href = 'mailto:' + text;
} else {
// do extended autolink path validation
do {
prevCapZero = cap[0];
cap[0] = this.rules.inline._backpedal.exec(cap[0])[0];
} while (prevCapZero !== cap[0]);
text = escape(cap[0]);
if (cap[1] === 'www.') {
href = 'http://' + text;
} else {
href = text;
}
}
src = src.substring(cap[0].length);
raw = cap[0];
tokens.push({
type: 'link',
raw,
text,
href,
tokens: [
{
type: 'text',
raw: text,
text
}
]
});
out += text;
continue;
}
// text
if (cap = this.rules.inline.text.exec(src)) {
src = src.substring(cap[0].length);
raw = cap[0];
if (this.inRawBlock) {
text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0];
} else {
text = escape(this.options.smartypants ? this.smartypants(cap[0]) : cap[0]);
}
tokens.push({
type: 'text',
raw,
text
});
out += text;
continue;
}
if (src) {
const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);
if (this.options.silent) {
console.error(errMsg);
} else {
throw new Error(errMsg);
}
}
}
return out;
}
escapes(text) {
return text ? text.replace(inline._escapes, '$1') : text;
}
/**
* tokenize Link
*/
outputLink(cap, link, tokens, raw) {
const href = link.href;
const title = link.title ? escape(link.title) : null;
const newTokens = tokens ? [] : null;
if (cap[0].charAt(0) !== '!') {
const text = this.inlineTokens(cap[1], newTokens);
tokens.push({
type: 'link',
raw,
text,
href,
title,
tokens: newTokens
});
return text;
} else {
const text = escape(cap[1]);
tokens.push({
type: 'image',
raw,
text,
href,
title
});
return text;
}
}
/**
* Smartypants Transformations
*/
smartypants(text) {
return text
// em-dashes
.replace(/---/g, '\u2014')
// en-dashes
.replace(/--/g, '\u2013')
// opening singles
.replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
// closing singles & apostrophes
.replace(/'/g, '\u2019')
// opening doubles
.replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
// closing doubles
.replace(/"/g, '\u201d')
// ellipses
.replace(/\.{3}/g, '\u2026');
}
/**
* Mangle Links
*/
mangle(text) {
let out = '',
i,
ch;
const l = text.length;
for (i = 0; i < l; i++) {
ch = text.charCodeAt(i);
if (Math.random() > 0.5) {
ch = 'x' + ch.toString(16);
}
out += '&#' + ch + ';';
}
return out;
}
};

View File

@ -1,10 +1,8 @@
const Renderer = require('./Renderer.js');
const Slugger = require('./Slugger.js');
const InlineLexer = require('./InlineLexer.js');
const TextRenderer = require('./TextRenderer.js');
const Slugger = require('./Slugger.js');
const { defaults } = require('./defaults.js');
const {
merge,
unescape
} = require('./helpers.js');
@ -13,12 +11,11 @@ const {
*/
module.exports = class Parser {
constructor(options) {
this.tokens = [];
this.token = null;
this.options = options || defaults;
this.options.renderer = this.options.renderer || new Renderer();
this.renderer = this.options.renderer;
this.renderer.options = this.options;
this.textRenderer = new TextRenderer();
this.slugger = new Slugger();
}
@ -28,179 +25,231 @@ module.exports = class Parser {
static parse(tokens, options) {
const parser = new Parser(options);
return parser.parse(tokens);
};
}
/**
* Parse Loop
*/
parse(tokens) {
this.inline = new InlineLexer(tokens.links, this.options);
// use an InlineLexer with a TextRenderer to extract pure text
this.inlineText = new InlineLexer(
tokens.links,
merge({}, this.options, { renderer: new TextRenderer() })
);
this.tokens = tokens.reverse();
parse(tokens, top = true) {
let out = '',
i,
j,
k,
l2,
l3,
row,
cell,
header,
body,
token,
ordered,
start,
loose,
itemBody,
item,
checked,
task,
checkbox;
let out = '';
while (this.next()) {
out += this.tok();
const l = tokens.length;
for (i = 0; i < l; i++) {
token = tokens[i];
switch (token.type) {
case 'space': {
continue;
}
case 'hr': {
out += this.renderer.hr();
continue;
}
case 'heading': {
out += this.renderer.heading(
this.parseInline(token.tokens),
token.depth,
unescape(this.parseInline(token.tokens, this.textRenderer)),
this.slugger);
continue;
}
case 'code': {
out += this.renderer.code(token.text,
token.lang,
token.escaped);
continue;
}
case 'table': {
header = '';
// header
cell = '';
l2 = token.header.length;
for (j = 0; j < l2; j++) {
cell += this.renderer.tablecell(
this.parseInline(token.tokens.header[j]),
{ header: true, align: token.align[j] }
);
}
header += this.renderer.tablerow(cell);
body = '';
l2 = token.cells.length;
for (j = 0; j < l2; j++) {
row = token.tokens.cells[j];
cell = '';
l3 = row.length;
for (k = 0; k < l3; k++) {
cell += this.renderer.tablecell(
this.parseInline(row[k]),
{ header: false, align: token.align[k] }
);
}
body += this.renderer.tablerow(cell);
}
out += this.renderer.table(header, body);
continue;
}
case 'blockquote': {
body = this.parse(token.tokens);
out += this.renderer.blockquote(body);
continue;
}
case 'list': {
ordered = token.ordered;
start = token.start;
loose = token.loose;
l2 = token.items.length;
body = '';
for (j = 0; j < l2; j++) {
item = token.items[j];
checked = item.checked;
task = item.task;
itemBody = '';
if (item.task) {
checkbox = this.renderer.checkbox(checked);
if (loose) {
if (item.tokens[0].type === 'text') {
item.tokens[0].text = checkbox + ' ' + item.tokens[0].text;
if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') {
item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text;
}
} else {
item.tokens.unshift({
type: 'text',
text: checkbox
});
}
} else {
itemBody += checkbox;
}
}
itemBody += this.parse(item.tokens, loose);
body += this.renderer.listitem(itemBody, task, checked);
}
out += this.renderer.list(body, ordered, start);
continue;
}
case 'html': {
// TODO parse inline content if parameter markdown=1
out += this.renderer.html(token.text);
continue;
}
case 'paragraph': {
out += this.renderer.paragraph(this.parseInline(token.tokens));
continue;
}
case 'text': {
body = token.tokens ? this.parseInline(token.tokens) : token.text;
while (i + 1 < l && tokens[i + 1].type === 'text') {
token = tokens[++i];
body += '\n' + (token.tokens ? this.parseInline(token.tokens) : token.text);
}
out += top ? this.renderer.paragraph(body) : body;
continue;
}
default: {
const errMsg = 'Token with "' + token.type + '" type was not found.';
if (this.options.silent) {
console.error(errMsg);
return;
} else {
throw new Error(errMsg);
}
}
}
}
return out;
};
}
/**
* Next Token
* Parse Inline Tokens
*/
next() {
this.token = this.tokens.pop();
return this.token;
};
parseInline(tokens, renderer) {
renderer = renderer || this.renderer;
let out = '',
i,
token;
/**
* Preview Next Token
*/
peek() {
return this.tokens[this.tokens.length - 1] || 0;
};
/**
* Parse Text Tokens
*/
parseText() {
let body = this.token.text;
while (this.peek().type === 'text') {
body += '\n' + this.next().text;
}
return this.inline.output(body);
};
/**
* Parse Current Token
*/
tok() {
let body = '';
switch (this.token.type) {
case 'space': {
return '';
}
case 'hr': {
return this.renderer.hr();
}
case 'heading': {
return this.renderer.heading(
this.inline.output(this.token.text),
this.token.depth,
unescape(this.inlineText.output(this.token.text)),
this.slugger);
}
case 'code': {
return this.renderer.code(this.token.text,
this.token.lang,
this.token.escaped);
}
case 'table': {
let header = '',
i,
row,
cell,
j;
// header
cell = '';
for (i = 0; i < this.token.header.length; i++) {
cell += this.renderer.tablecell(
this.inline.output(this.token.header[i]),
{ header: true, align: this.token.align[i] }
);
const l = tokens.length;
for (i = 0; i < l; i++) {
token = tokens[i];
switch (token.type) {
case 'escape': {
out += token.text;
break;
}
header += this.renderer.tablerow(cell);
for (i = 0; i < this.token.cells.length; i++) {
row = this.token.cells[i];
cell = '';
for (j = 0; j < row.length; j++) {
cell += this.renderer.tablecell(
this.inline.output(row[j]),
{ header: false, align: this.token.align[j] }
);
}
body += this.renderer.tablerow(cell);
case 'html': {
out += renderer.html(token.text);
break;
}
return this.renderer.table(header, body);
}
case 'blockquote_start': {
body = '';
while (this.next().type !== 'blockquote_end') {
body += this.tok();
case 'link': {
out += renderer.link(token.href, token.title, this.parseInline(token.tokens, renderer));
break;
}
return this.renderer.blockquote(body);
}
case 'list_start': {
body = '';
const ordered = this.token.ordered,
start = this.token.start;
while (this.next().type !== 'list_end') {
body += this.tok();
case 'image': {
out += renderer.image(token.href, token.title, token.text);
break;
}
return this.renderer.list(body, ordered, start);
}
case 'list_item_start': {
body = '';
const loose = this.token.loose;
const checked = this.token.checked;
const task = this.token.task;
if (this.token.task) {
if (loose) {
if (this.peek().type === 'text') {
const nextToken = this.peek();
nextToken.text = this.renderer.checkbox(checked) + ' ' + nextToken.text;
} else {
this.tokens.push({
type: 'text',
text: this.renderer.checkbox(checked)
});
}
case 'strong': {
out += renderer.strong(this.parseInline(token.tokens, renderer));
break;
}
case 'em': {
out += renderer.em(this.parseInline(token.tokens, renderer));
break;
}
case 'codespan': {
out += renderer.codespan(token.text);
break;
}
case 'br': {
out += renderer.br();
break;
}
case 'del': {
out += renderer.del(this.parseInline(token.tokens, renderer));
break;
}
case 'text': {
out += renderer.text(token.text);
break;
}
default: {
const errMsg = 'Token with "' + token.type + '" type was not found.';
if (this.options.silent) {
console.error(errMsg);
return;
} else {
body += this.renderer.checkbox(checked);
throw new Error(errMsg);
}
}
while (this.next().type !== 'list_item_end') {
body += !loose && this.token.type === 'text'
? this.parseText()
: this.tok();
}
return this.renderer.listitem(body, task, checked);
}
case 'html': {
// TODO parse inline content if parameter markdown=1
return this.renderer.html(this.token.text);
}
case 'paragraph': {
return this.renderer.paragraph(this.inline.output(this.token.text));
}
case 'text': {
return this.renderer.paragraph(this.parseText());
}
default: {
const errMsg = 'Token with "' + this.token.type + '" type was not found.';
if (this.options.silent) {
console.log(errMsg);
} else {
throw new Error(errMsg);
}
}
}
};
return out;
}
};

View File

@ -34,15 +34,15 @@ module.exports = class Renderer {
+ '">'
+ (escaped ? code : escape(code, true))
+ '</code></pre>\n';
};
}
blockquote(quote) {
return '<blockquote>\n' + quote + '</blockquote>\n';
};
}
html(html) {
return html;
};
}
heading(text, level, raw, slugger) {
if (this.options.headerIds) {
@ -59,21 +59,21 @@ module.exports = class Renderer {
}
// ignore IDs
return '<h' + level + '>' + text + '</h' + level + '>\n';
};
}
hr() {
return this.options.xhtml ? '<hr/>\n' : '<hr>\n';
};
}
list(body, ordered, start) {
const type = ordered ? 'ol' : 'ul',
startatt = (ordered && start !== 1) ? (' start="' + start + '"') : '';
return '<' + type + startatt + '>\n' + body + '</' + type + '>\n';
};
}
listitem(text) {
return '<li>' + text + '</li>\n';
};
}
checkbox(checked) {
return '<input '
@ -81,11 +81,11 @@ module.exports = class Renderer {
+ 'disabled="" type="checkbox"'
+ (this.options.xhtml ? ' /' : '')
+ '> ';
};
}
paragraph(text) {
return '<p>' + text + '</p>\n';
};
}
table(header, body) {
if (body) body = '<tbody>' + body + '</tbody>';
@ -96,11 +96,11 @@ module.exports = class Renderer {
+ '</thead>\n'
+ body
+ '</table>\n';
};
}
tablerow(content) {
return '<tr>\n' + content + '</tr>\n';
};
}
tablecell(content, flags) {
const type = flags.header ? 'th' : 'td';
@ -108,28 +108,28 @@ module.exports = class Renderer {
? '<' + type + ' align="' + flags.align + '">'
: '<' + type + '>';
return tag + content + '</' + type + '>\n';
};
}
// span level renderer
strong(text) {
return '<strong>' + text + '</strong>';
};
}
em(text) {
return '<em>' + text + '</em>';
};
}
codespan(text) {
return '<code>' + text + '</code>';
};
}
br() {
return this.options.xhtml ? '<br/>' : '<br>';
};
}
del(text) {
return '<del>' + text + '</del>';
};
}
link(href, title, text) {
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
@ -142,7 +142,7 @@ module.exports = class Renderer {
}
out += '>' + text + '</a>';
return out;
};
}
image(href, title, text) {
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
@ -156,9 +156,9 @@ module.exports = class Renderer {
}
out += this.options.xhtml ? '/>' : '>';
return out;
};
}
text(text) {
return text;
};
}
};

View File

@ -29,5 +29,5 @@ module.exports = class Slugger {
this.seen[slug] = 0;
return slug;
};
}
};

View File

@ -2,7 +2,6 @@ const Lexer = require('./Lexer.js');
const Parser = require('./Parser.js');
const Renderer = require('./Renderer.js');
const TextRenderer = require('./TextRenderer.js');
const InlineLexer = require('./InlineLexer.js');
const Slugger = require('./Slugger.js');
const {
merge,
@ -140,9 +139,6 @@ marked.TextRenderer = TextRenderer;
marked.Lexer = Lexer;
marked.lexer = Lexer.lex;
marked.InlineLexer = InlineLexer;
marked.inlineLexer = InlineLexer.output;
marked.Slugger = Slugger;
marked.parse = marked;

View File

@ -21,6 +21,22 @@ beforeEach(() => {
return result;
}
};
},
toEqualHtml: () => {
return {
compare: async(actual, expected) => {
const result = {};
result.pass = await htmlDiffer.isEqual(expected, actual);
if (result.pass) {
result.message = `Expected '${actual}' not to equal '${expected}'`;
} else {
const diff = await htmlDiffer.firstDiff(actual, expected);
result.message = `Expected: ${diff.expected}\n Actual: ${diff.actual}`;
}
return result;
}
};
}
});
});

1019
test/unit/Lexer-spec.js Normal file

File diff suppressed because it is too large Load Diff

449
test/unit/Parser-spec.js Normal file
View File

@ -0,0 +1,449 @@
const Parser = require('../../src/Parser.js');
async function expectHtml({ tokens, options, html, inline }) {
const parser = new Parser(options);
const actual = parser[inline ? 'parseInline' : 'parse'](tokens);
await expectAsync(actual).toEqualHtml(html);
}
describe('Parser', () => {
describe('block', () => {
it('space between paragraphs', async() => {
await expectHtml({
tokens: [
{
type: 'paragraph',
text: 'paragraph 1',
tokens: [
{ type: 'text', text: 'paragraph 1' }
]
},
{ type: 'space' },
{
type: 'paragraph',
text: 'paragraph 2',
tokens: [
{ type: 'text', text: 'paragraph 2' }
]
}
],
html: '<p>paragraph 1</p><p>paragraph 2</p>'
});
});
it('hr', async() => {
await expectHtml({
tokens: [{
type: 'hr'
}],
html: '<hr />'
});
});
it('heading', async() => {
await expectHtml({
tokens: [{
type: 'heading',
depth: 1,
text: 'heading',
tokens: [
{ type: 'text', text: 'heading' }
]
}],
html: '<h1 id="heading">heading</h1>'
});
});
it('code', async() => {
await expectHtml({
tokens: [{
type: 'code',
text: 'code'
}],
html: '<pre><code>code</code></pre>'
});
});
it('table', async() => {
await expectHtml({
tokens: [{
type: 'table',
header: ['a', 'b'],
align: ['left', 'right'],
cells: [['1', '2']],
tokens: {
header: [
[{ type: 'text', text: 'a' }],
[{ type: 'text', text: 'b' }]
],
cells: [
[
[{ type: 'text', text: '1' }],
[{ type: 'text', text: '2' }]
]
]
}
}],
html: `
<table>
<thead>
<tr>
<th align="left">a</th>
<th align="right">b</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">1</td>
<td align="right">2</td>
</tr>
</tbody>
</table>`
});
});
it('blockquote', async() => {
await expectHtml({
tokens: [
{
type: 'blockquote',
tokens: [{
type: 'paragraph',
text: 'blockquote',
tokens: [
{ type: 'text', text: 'blockquote' }
]
}]
}
],
html: '<blockquote><p>blockquote</p></blockquote>'
});
});
describe('list', () => {
it('unordered', async() => {
await expectHtml({
tokens: [
{
type: 'list',
ordered: false,
start: '',
loose: false,
items: [
{
task: false,
checked: undefined,
tokens: [{
type: 'text',
text: 'item 1',
tokens: [{ type: 'text', text: 'item 1' }]
}]
},
{
task: false,
checked: undefined,
tokens: [{
type: 'text',
text: 'item 2',
tokens: [{ type: 'text', text: 'item 2' }]
}]
}
]
}
],
html: `
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>`
});
});
it('ordered', async() => {
await expectHtml({
tokens: [
{
type: 'list',
ordered: true,
start: 2,
loose: false,
items: [
{
task: false,
checked: undefined,
tokens: [{
type: 'text',
text: 'item 1',
tokens: [{ type: 'text', text: 'item 1' }]
}]
},
{
task: false,
checked: undefined,
tokens: [{
type: 'text',
text: 'item 2',
tokens: [{ type: 'text', text: 'item 2' }]
}]
}
]
}
],
html: `
<ol start='2'>
<li>item 1</li>
<li>item 2</li>
</ol>`
});
});
it('tasks', async() => {
await expectHtml({
tokens: [
{
type: 'list',
ordered: false,
start: '',
loose: false,
items: [
{
task: true,
checked: false,
tokens: [{
type: 'text',
text: 'item 1',
tokens: [{ type: 'text', text: 'item 1' }]
}]
},
{
task: true,
checked: true,
tokens: [{
type: 'text',
text: 'item 2',
tokens: [{ type: 'text', text: 'item 2' }]
}]
}
]
}
],
html: `
<ul>
<li><input disabled type="checkbox"> item 1</li>
<li><input checked disabled type="checkbox"> item 2</li>
</ul>`
});
});
it('loose', async() => {
await expectHtml({
tokens: [
{
type: 'list',
ordered: false,
start: '',
loose: true,
items: [
{
task: false,
checked: undefined,
tokens: [{
type: 'text',
text: 'item 1',
tokens: [{ type: 'text', text: 'item 1' }]
}]
},
{
task: false,
checked: undefined,
tokens: [{
type: 'text',
text: 'item 2',
tokens: [{ type: 'text', text: 'item 2' }]
}]
}
]
}
],
html: `
<ul>
<li><p>item 1</p></li>
<li><p>item 2</p></li>
</ul>`
});
});
});
it('html', async() => {
await expectHtml({
tokens: [{
type: 'html',
text: '<div>html</div>'
}],
html: '<div>html</div>'
});
});
it('paragraph', async() => {
await expectHtml({
tokens: [{
type: 'paragraph',
text: 'paragraph 1',
tokens: [
{ type: 'text', text: 'paragraph 1' }
]
}],
html: '<p>paragraph 1</p>'
});
});
it('text', async() => {
await expectHtml({
tokens: [
{ type: 'text', text: 'text 1' },
{ type: 'text', text: 'text 2' }
],
html: '<p>text 1\ntext 2</p>'
});
});
});
describe('inline', () => {
it('escape', async() => {
await expectHtml({
inline: true,
tokens: [
{ type: 'escape', text: '&gt;' }
],
html: '&gt;'
});
});
it('html', async() => {
await expectHtml({
inline: true,
tokens: [
{ type: 'html', text: '<div>' },
{ type: 'text', text: 'html' },
{ type: 'html', text: '</div>' }
],
html: '<div>html</div>'
});
});
it('link', async() => {
await expectHtml({
inline: true,
tokens: [
{
type: 'link',
text: 'link',
href: 'https://example.com',
title: 'title',
tokens: [
{ type: 'text', text: 'link' }
]
}
],
html: '<a href="https://example.com" title="title">link</a>'
});
});
it('image', async() => {
await expectHtml({
inline: true,
tokens: [
{
type: 'image',
text: 'image',
href: 'image.png',
title: 'title'
}
],
html: '<img src="image.png" alt="image" title="title">'
});
});
it('strong', async() => {
await expectHtml({
inline: true,
tokens: [
{
type: 'strong',
text: 'strong',
tokens: [
{ type: 'text', text: 'strong' }
]
}
],
html: '<strong>strong</strong>'
});
});
it('em', async() => {
await expectHtml({
inline: true,
tokens: [
{
type: 'em',
text: 'em',
tokens: [
{ type: 'text', text: 'em' }
]
}
],
html: '<em>em</em>'
});
});
it('codespan', async() => {
await expectHtml({
inline: true,
tokens: [
{
type: 'codespan',
text: 'code'
}
],
html: '<code>code</code>'
});
});
it('br', async() => {
await expectHtml({
inline: true,
tokens: [{
type: 'br'
}],
html: '<br />'
});
});
it('del', async() => {
await expectHtml({
inline: true,
tokens: [
{
type: 'del',
text: 'del',
tokens: [
{ type: 'text', text: 'del' }
]
}
],
html: '<del>del</del>'
});
});
it('text', async() => {
await expectHtml({
inline: true,
tokens: [
{ type: 'text', text: 'text 1' },
{ type: 'text', text: 'text 2' }
],
html: 'text 1text 2'
});
});
});
});

View File

@ -58,6 +58,11 @@ describe('Test slugger functionality', () => {
const slugger = new marked.Slugger();
expect(slugger.slug('file.txt')).toBe('filetxt');
});
it('should remove html tags', () => {
const slugger = new marked.Slugger();
expect(slugger.slug('<em>html</em>')).toBe('html');
});
});
describe('Test paragraph token type', () => {
@ -67,8 +72,8 @@ describe('Test paragraph token type', () => {
const tokens = marked.lexer(md);
expect(tokens[0].type).toBe('paragraph');
expect(tokens[3].type).toBe('paragraph');
expect(tokens[7].type).toBe('text');
expect(tokens[2].tokens[0].type).toBe('paragraph');
expect(tokens[3].items[0].tokens[0].type).toBe('text');
});
});