feat: add Marked instance (#2831)
Co-authored-by: Steven <steven@ceriously.com>
This commit is contained in:
parent
f19fe76db9
commit
353e13b479
@ -1,3 +1,18 @@
|
||||
## Marked instance
|
||||
|
||||
By default, Marked stores options and extensions in the global scope. That means changing the options in one script will also change the options in another script since they share the same instance.
|
||||
|
||||
If you don't want to mutate global scope, you can create a new instance of Marked to ensure options and extensions are locally scoped.
|
||||
|
||||
```js
|
||||
import { Marked } from 'marked';
|
||||
const marked = new Marked([options, extension, ...]);
|
||||
```
|
||||
|
||||
|Argument |Type |Notes |
|
||||
|:--------|:-------|:----------------------------------------------------------------------|
|
||||
| options |`object`|The same arguments that can be passed to [`marked.use`](/using_pro#use)|
|
||||
|
||||
## The `parse` function
|
||||
|
||||
```js
|
||||
@ -162,6 +177,7 @@ markedWorker.onmessage = (e) => {
|
||||
|
||||
markedWorker.postMessage(markdownString);
|
||||
```
|
||||
|
||||
<h2 id="cli-extensions">CLI Extensions</h2>
|
||||
|
||||
You can use extensions in the CLI by creating a new CLI that imports marked and the marked binary.
|
||||
|
@ -22,7 +22,7 @@ marked.use({
|
||||
|
||||
You can also supply multiple `extension` objects at once.
|
||||
|
||||
```
|
||||
```js
|
||||
marked.use(myExtension, extension2, extension3);
|
||||
|
||||
\\ EQUIVALENT TO:
|
||||
@ -30,12 +30,11 @@ marked.use(myExtension, extension2, extension3);
|
||||
marked.use(myExtension);
|
||||
marked.use(extension2);
|
||||
marked.use(extension3);
|
||||
|
||||
```
|
||||
|
||||
All options will overwrite those previously set, except for the following options which will be merged with the existing framework and can be used to change or extend the functionality of Marked: `renderer`, `tokenizer`, `walkTokens`, and `extensions`.
|
||||
All options will overwrite those previously set, except for the following options which will be merged with the existing framework and can be used to change or extend the functionality of Marked: `renderer`, `tokenizer`, `hooks`, `walkTokens`, and `extensions`.
|
||||
|
||||
* The `renderer` and `tokenizer` options are objects with functions that will be merged into the built-in `renderer` and `tokenizer` respectively.
|
||||
* The `renderer`, `tokenizer`, and `hooks` options are objects with functions that will be merged into the built-in `renderer` and `tokenizer` respectively.
|
||||
|
||||
* The `walkTokens` option is a function that will be called to post-process every token before rendering.
|
||||
|
||||
|
374
src/Instance.js
Normal file
374
src/Instance.js
Normal file
@ -0,0 +1,374 @@
|
||||
import { getDefaults } from './defaults.js';
|
||||
import { Lexer } from './Lexer.js';
|
||||
import { Parser } from './Parser.js';
|
||||
import { Hooks } from './Hooks.js';
|
||||
import { Renderer } from './Renderer.js';
|
||||
import { Tokenizer } from './Tokenizer.js';
|
||||
import { TextRenderer } from './TextRenderer.js';
|
||||
import { Slugger } from './Slugger.js';
|
||||
import {
|
||||
checkDeprecations,
|
||||
escape
|
||||
} from './helpers.js';
|
||||
|
||||
export class Marked {
|
||||
defaults = getDefaults();
|
||||
options = this.setOptions;
|
||||
|
||||
parse = this.#parseMarkdown(Lexer.lex, Parser.parse);
|
||||
parseInline = this.#parseMarkdown(Lexer.lexInline, Parser.parseInline);
|
||||
|
||||
Parser = Parser;
|
||||
parser = Parser.parse;
|
||||
Renderer = Renderer;
|
||||
TextRenderer = TextRenderer;
|
||||
Lexer = Lexer;
|
||||
lexer = Lexer.lex;
|
||||
Tokenizer = Tokenizer;
|
||||
Slugger = Slugger;
|
||||
Hooks = Hooks;
|
||||
|
||||
constructor(...args) {
|
||||
this.use(...args);
|
||||
}
|
||||
|
||||
walkTokens(tokens, callback) {
|
||||
let values = [];
|
||||
for (const token of tokens) {
|
||||
values = values.concat(callback.call(this, token));
|
||||
switch (token.type) {
|
||||
case 'table': {
|
||||
for (const cell of token.header) {
|
||||
values = values.concat(this.walkTokens(cell.tokens, callback));
|
||||
}
|
||||
for (const row of token.rows) {
|
||||
for (const cell of row) {
|
||||
values = values.concat(this.walkTokens(cell.tokens, callback));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'list': {
|
||||
values = values.concat(this.walkTokens(token.items, callback));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (this.defaults.extensions && this.defaults.extensions.childTokens && this.defaults.extensions.childTokens[token.type]) { // Walk any extensions
|
||||
this.defaults.extensions.childTokens[token.type].forEach((childTokens) => {
|
||||
values = values.concat(this.walkTokens(token[childTokens], callback));
|
||||
});
|
||||
} else if (token.tokens) {
|
||||
values = values.concat(this.walkTokens(token.tokens, callback));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
use(...args) {
|
||||
const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} };
|
||||
|
||||
args.forEach((pack) => {
|
||||
// copy options to new object
|
||||
const opts = { ...pack };
|
||||
|
||||
// set async to true if it was set to true before
|
||||
opts.async = this.defaults.async || opts.async || false;
|
||||
|
||||
// ==-- Parse "addon" extensions --== //
|
||||
if (pack.extensions) {
|
||||
pack.extensions.forEach((ext) => {
|
||||
if (!ext.name) {
|
||||
throw new Error('extension name required');
|
||||
}
|
||||
if (ext.renderer) { // Renderer extensions
|
||||
const prevRenderer = extensions.renderers[ext.name];
|
||||
if (prevRenderer) {
|
||||
// Replace extension with func to run new extension but fall back if false
|
||||
extensions.renderers[ext.name] = function(...args) {
|
||||
let ret = ext.renderer.apply(this, args);
|
||||
if (ret === false) {
|
||||
ret = prevRenderer.apply(this, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
} else {
|
||||
extensions.renderers[ext.name] = ext.renderer;
|
||||
}
|
||||
}
|
||||
if (ext.tokenizer) { // Tokenizer Extensions
|
||||
if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {
|
||||
throw new Error("extension level must be 'block' or 'inline'");
|
||||
}
|
||||
if (extensions[ext.level]) {
|
||||
extensions[ext.level].unshift(ext.tokenizer);
|
||||
} else {
|
||||
extensions[ext.level] = [ext.tokenizer];
|
||||
}
|
||||
if (ext.start) { // Function to check for start of token
|
||||
if (ext.level === 'block') {
|
||||
if (extensions.startBlock) {
|
||||
extensions.startBlock.push(ext.start);
|
||||
} else {
|
||||
extensions.startBlock = [ext.start];
|
||||
}
|
||||
} else if (ext.level === 'inline') {
|
||||
if (extensions.startInline) {
|
||||
extensions.startInline.push(ext.start);
|
||||
} else {
|
||||
extensions.startInline = [ext.start];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ext.childTokens) { // Child tokens to be visited by walkTokens
|
||||
extensions.childTokens[ext.name] = ext.childTokens;
|
||||
}
|
||||
});
|
||||
opts.extensions = extensions;
|
||||
}
|
||||
|
||||
// ==-- Parse "overwrite" extensions --== //
|
||||
if (pack.renderer) {
|
||||
const renderer = this.defaults.renderer || new Renderer(this.defaults);
|
||||
for (const prop in pack.renderer) {
|
||||
const prevRenderer = renderer[prop];
|
||||
// Replace renderer with func to run extension, but fall back if false
|
||||
renderer[prop] = (...args) => {
|
||||
let ret = pack.renderer[prop].apply(renderer, args);
|
||||
if (ret === false) {
|
||||
ret = prevRenderer.apply(renderer, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
opts.renderer = renderer;
|
||||
}
|
||||
if (pack.tokenizer) {
|
||||
const tokenizer = this.defaults.tokenizer || new Tokenizer(this.defaults);
|
||||
for (const prop in pack.tokenizer) {
|
||||
const prevTokenizer = tokenizer[prop];
|
||||
// Replace tokenizer with func to run extension, but fall back if false
|
||||
tokenizer[prop] = (...args) => {
|
||||
let ret = pack.tokenizer[prop].apply(tokenizer, args);
|
||||
if (ret === false) {
|
||||
ret = prevTokenizer.apply(tokenizer, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
opts.tokenizer = tokenizer;
|
||||
}
|
||||
|
||||
// ==-- Parse Hooks extensions --== //
|
||||
if (pack.hooks) {
|
||||
const hooks = this.defaults.hooks || new Hooks();
|
||||
for (const prop in pack.hooks) {
|
||||
const prevHook = hooks[prop];
|
||||
if (Hooks.passThroughHooks.has(prop)) {
|
||||
hooks[prop] = (arg) => {
|
||||
if (this.defaults.async) {
|
||||
return Promise.resolve(pack.hooks[prop].call(hooks, arg)).then(ret => {
|
||||
return prevHook.call(hooks, ret);
|
||||
});
|
||||
}
|
||||
|
||||
const ret = pack.hooks[prop].call(hooks, arg);
|
||||
return prevHook.call(hooks, ret);
|
||||
};
|
||||
} else {
|
||||
hooks[prop] = (...args) => {
|
||||
let ret = pack.hooks[prop].apply(hooks, args);
|
||||
if (ret === false) {
|
||||
ret = prevHook.apply(hooks, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
}
|
||||
opts.hooks = hooks;
|
||||
}
|
||||
|
||||
// ==-- Parse WalkTokens extensions --== //
|
||||
if (pack.walkTokens) {
|
||||
const walkTokens = this.defaults.walkTokens;
|
||||
opts.walkTokens = function(token) {
|
||||
let values = [];
|
||||
values.push(pack.walkTokens.call(this, token));
|
||||
if (walkTokens) {
|
||||
values = values.concat(walkTokens.call(this, token));
|
||||
}
|
||||
return values;
|
||||
};
|
||||
}
|
||||
|
||||
this.defaults = { ...this.defaults, ...opts };
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setOptions(opt) {
|
||||
this.defaults = { ...this.defaults, ...opt };
|
||||
return this;
|
||||
}
|
||||
|
||||
#parseMarkdown(lexer, parser) {
|
||||
return (src, opt, callback) => {
|
||||
if (typeof opt === 'function') {
|
||||
callback = opt;
|
||||
opt = null;
|
||||
}
|
||||
|
||||
const origOpt = { ...opt };
|
||||
opt = { ...this.defaults, ...origOpt };
|
||||
const throwError = this.#onError(opt.silent, opt.async, callback);
|
||||
|
||||
// throw error in case of non string input
|
||||
if (typeof src === 'undefined' || src === null) {
|
||||
return throwError(new Error('marked(): input parameter is undefined or null'));
|
||||
}
|
||||
if (typeof src !== 'string') {
|
||||
return throwError(new Error('marked(): input parameter is of type '
|
||||
+ Object.prototype.toString.call(src) + ', string expected'));
|
||||
}
|
||||
|
||||
checkDeprecations(opt, callback);
|
||||
|
||||
if (opt.hooks) {
|
||||
opt.hooks.options = opt;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
const highlight = opt.highlight;
|
||||
let tokens;
|
||||
|
||||
try {
|
||||
if (opt.hooks) {
|
||||
src = opt.hooks.preprocess(src);
|
||||
}
|
||||
tokens = lexer(src, opt);
|
||||
} catch (e) {
|
||||
return throwError(e);
|
||||
}
|
||||
|
||||
const done = (err) => {
|
||||
let out;
|
||||
|
||||
if (!err) {
|
||||
try {
|
||||
if (opt.walkTokens) {
|
||||
this.walkTokens(tokens, opt.walkTokens);
|
||||
}
|
||||
out = parser(tokens, opt);
|
||||
if (opt.hooks) {
|
||||
out = opt.hooks.postprocess(out);
|
||||
}
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
opt.highlight = highlight;
|
||||
|
||||
return err
|
||||
? throwError(err)
|
||||
: callback(null, out);
|
||||
};
|
||||
|
||||
if (!highlight || highlight.length < 3) {
|
||||
return done();
|
||||
}
|
||||
|
||||
delete opt.highlight;
|
||||
|
||||
if (!tokens.length) return done();
|
||||
|
||||
let pending = 0;
|
||||
this.walkTokens(tokens, (token) => {
|
||||
if (token.type === 'code') {
|
||||
pending++;
|
||||
setTimeout(() => {
|
||||
highlight(token.text, token.lang, (err, code) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
if (code != null && code !== token.text) {
|
||||
token.text = code;
|
||||
token.escaped = true;
|
||||
}
|
||||
|
||||
pending--;
|
||||
if (pending === 0) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
if (pending === 0) {
|
||||
done();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (opt.async) {
|
||||
return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src)
|
||||
.then(src => lexer(src, opt))
|
||||
.then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens)
|
||||
.then(tokens => parser(tokens, opt))
|
||||
.then(html => opt.hooks ? opt.hooks.postprocess(html) : html)
|
||||
.catch(throwError);
|
||||
}
|
||||
|
||||
try {
|
||||
if (opt.hooks) {
|
||||
src = opt.hooks.preprocess(src);
|
||||
}
|
||||
const tokens = lexer(src, opt);
|
||||
if (opt.walkTokens) {
|
||||
this.walkTokens(tokens, opt.walkTokens);
|
||||
}
|
||||
let html = parser(tokens, opt);
|
||||
if (opt.hooks) {
|
||||
html = opt.hooks.postprocess(html);
|
||||
}
|
||||
return html;
|
||||
} catch (e) {
|
||||
return throwError(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#onError(silent, async, callback) {
|
||||
return (e) => {
|
||||
e.message += '\nPlease report this to https://github.com/markedjs/this.';
|
||||
|
||||
if (silent) {
|
||||
const msg = '<p>An error occurred:</p><pre>'
|
||||
+ escape(e.message + '', true)
|
||||
+ '</pre>';
|
||||
if (async) {
|
||||
return Promise.resolve(msg);
|
||||
}
|
||||
if (callback) {
|
||||
callback(null, msg);
|
||||
return;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (async) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
if (callback) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
};
|
||||
}
|
||||
}
|
351
src/marked.js
351
src/marked.js
@ -5,179 +5,16 @@ import { Renderer } from './Renderer.js';
|
||||
import { TextRenderer } from './TextRenderer.js';
|
||||
import { Slugger } from './Slugger.js';
|
||||
import { Hooks } from './Hooks.js';
|
||||
import {
|
||||
checkDeprecations,
|
||||
escape
|
||||
} from './helpers.js';
|
||||
import {
|
||||
getDefaults,
|
||||
changeDefaults,
|
||||
defaults
|
||||
} from './defaults.js';
|
||||
import { Marked } from './Instance.js';
|
||||
import { changeDefaults, getDefaults, defaults } from './defaults.js';
|
||||
|
||||
function onError(silent, async, callback) {
|
||||
return (e) => {
|
||||
e.message += '\nPlease report this to https://github.com/markedjs/marked.';
|
||||
|
||||
if (silent) {
|
||||
const msg = '<p>An error occurred:</p><pre>'
|
||||
+ escape(e.message + '', true)
|
||||
+ '</pre>';
|
||||
if (async) {
|
||||
return Promise.resolve(msg);
|
||||
}
|
||||
if (callback) {
|
||||
callback(null, msg);
|
||||
return;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (async) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
if (callback) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
};
|
||||
}
|
||||
|
||||
function parseMarkdown(lexer, parser) {
|
||||
return (src, opt, callback) => {
|
||||
if (typeof opt === 'function') {
|
||||
callback = opt;
|
||||
opt = null;
|
||||
}
|
||||
|
||||
const origOpt = { ...opt };
|
||||
opt = { ...marked.defaults, ...origOpt };
|
||||
const throwError = onError(opt.silent, opt.async, callback);
|
||||
|
||||
// throw error in case of non string input
|
||||
if (typeof src === 'undefined' || src === null) {
|
||||
return throwError(new Error('marked(): input parameter is undefined or null'));
|
||||
}
|
||||
if (typeof src !== 'string') {
|
||||
return throwError(new Error('marked(): input parameter is of type '
|
||||
+ Object.prototype.toString.call(src) + ', string expected'));
|
||||
}
|
||||
|
||||
checkDeprecations(opt, callback);
|
||||
|
||||
if (opt.hooks) {
|
||||
opt.hooks.options = opt;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
const highlight = opt.highlight;
|
||||
let tokens;
|
||||
|
||||
try {
|
||||
if (opt.hooks) {
|
||||
src = opt.hooks.preprocess(src);
|
||||
}
|
||||
tokens = lexer(src, opt);
|
||||
} catch (e) {
|
||||
return throwError(e);
|
||||
}
|
||||
|
||||
const done = function(err) {
|
||||
let out;
|
||||
|
||||
if (!err) {
|
||||
try {
|
||||
if (opt.walkTokens) {
|
||||
marked.walkTokens(tokens, opt.walkTokens);
|
||||
}
|
||||
out = parser(tokens, opt);
|
||||
if (opt.hooks) {
|
||||
out = opt.hooks.postprocess(out);
|
||||
}
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
opt.highlight = highlight;
|
||||
|
||||
return err
|
||||
? throwError(err)
|
||||
: callback(null, out);
|
||||
};
|
||||
|
||||
if (!highlight || highlight.length < 3) {
|
||||
return done();
|
||||
}
|
||||
|
||||
delete opt.highlight;
|
||||
|
||||
if (!tokens.length) return done();
|
||||
|
||||
let pending = 0;
|
||||
marked.walkTokens(tokens, function(token) {
|
||||
if (token.type === 'code') {
|
||||
pending++;
|
||||
setTimeout(() => {
|
||||
highlight(token.text, token.lang, function(err, code) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
if (code != null && code !== token.text) {
|
||||
token.text = code;
|
||||
token.escaped = true;
|
||||
}
|
||||
|
||||
pending--;
|
||||
if (pending === 0) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
if (pending === 0) {
|
||||
done();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (opt.async) {
|
||||
return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src)
|
||||
.then(src => lexer(src, opt))
|
||||
.then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens)
|
||||
.then(tokens => parser(tokens, opt))
|
||||
.then(html => opt.hooks ? opt.hooks.postprocess(html) : html)
|
||||
.catch(throwError);
|
||||
}
|
||||
|
||||
try {
|
||||
if (opt.hooks) {
|
||||
src = opt.hooks.preprocess(src);
|
||||
}
|
||||
const tokens = lexer(src, opt);
|
||||
if (opt.walkTokens) {
|
||||
marked.walkTokens(tokens, opt.walkTokens);
|
||||
}
|
||||
let html = parser(tokens, opt);
|
||||
if (opt.hooks) {
|
||||
html = opt.hooks.postprocess(html);
|
||||
}
|
||||
return html;
|
||||
} catch (e) {
|
||||
return throwError(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
const markedInstance = new Marked(defaults);
|
||||
|
||||
/**
|
||||
* Marked
|
||||
*/
|
||||
export function marked(src, opt, callback) {
|
||||
return parseMarkdown(Lexer.lex, Parser.parse)(src, opt, callback);
|
||||
return markedInstance.parse(src, opt, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,7 +23,8 @@ export function marked(src, opt, callback) {
|
||||
|
||||
marked.options =
|
||||
marked.setOptions = function(opt) {
|
||||
marked.defaults = { ...marked.defaults, ...opt };
|
||||
markedInstance.setOptions(opt);
|
||||
marked.defaults = markedInstance.defaults;
|
||||
changeDefaults(marked.defaults);
|
||||
return marked;
|
||||
};
|
||||
@ -200,144 +38,10 @@ marked.defaults = defaults;
|
||||
*/
|
||||
|
||||
marked.use = function(...args) {
|
||||
const extensions = marked.defaults.extensions || { renderers: {}, childTokens: {} };
|
||||
|
||||
args.forEach((pack) => {
|
||||
// copy options to new object
|
||||
const opts = { ...pack };
|
||||
|
||||
// set async to true if it was set to true before
|
||||
opts.async = marked.defaults.async || opts.async || false;
|
||||
|
||||
// ==-- Parse "addon" extensions --== //
|
||||
if (pack.extensions) {
|
||||
pack.extensions.forEach((ext) => {
|
||||
if (!ext.name) {
|
||||
throw new Error('extension name required');
|
||||
}
|
||||
if (ext.renderer) { // Renderer extensions
|
||||
const prevRenderer = extensions.renderers[ext.name];
|
||||
if (prevRenderer) {
|
||||
// Replace extension with func to run new extension but fall back if false
|
||||
extensions.renderers[ext.name] = function(...args) {
|
||||
let ret = ext.renderer.apply(this, args);
|
||||
if (ret === false) {
|
||||
ret = prevRenderer.apply(this, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
} else {
|
||||
extensions.renderers[ext.name] = ext.renderer;
|
||||
}
|
||||
}
|
||||
if (ext.tokenizer) { // Tokenizer Extensions
|
||||
if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {
|
||||
throw new Error("extension level must be 'block' or 'inline'");
|
||||
}
|
||||
if (extensions[ext.level]) {
|
||||
extensions[ext.level].unshift(ext.tokenizer);
|
||||
} else {
|
||||
extensions[ext.level] = [ext.tokenizer];
|
||||
}
|
||||
if (ext.start) { // Function to check for start of token
|
||||
if (ext.level === 'block') {
|
||||
if (extensions.startBlock) {
|
||||
extensions.startBlock.push(ext.start);
|
||||
} else {
|
||||
extensions.startBlock = [ext.start];
|
||||
}
|
||||
} else if (ext.level === 'inline') {
|
||||
if (extensions.startInline) {
|
||||
extensions.startInline.push(ext.start);
|
||||
} else {
|
||||
extensions.startInline = [ext.start];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ext.childTokens) { // Child tokens to be visited by walkTokens
|
||||
extensions.childTokens[ext.name] = ext.childTokens;
|
||||
}
|
||||
});
|
||||
opts.extensions = extensions;
|
||||
}
|
||||
|
||||
// ==-- Parse "overwrite" extensions --== //
|
||||
if (pack.renderer) {
|
||||
const renderer = marked.defaults.renderer || new Renderer();
|
||||
for (const prop in pack.renderer) {
|
||||
const prevRenderer = renderer[prop];
|
||||
// Replace renderer with func to run extension, but fall back if false
|
||||
renderer[prop] = (...args) => {
|
||||
let ret = pack.renderer[prop].apply(renderer, args);
|
||||
if (ret === false) {
|
||||
ret = prevRenderer.apply(renderer, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
opts.renderer = renderer;
|
||||
}
|
||||
if (pack.tokenizer) {
|
||||
const tokenizer = marked.defaults.tokenizer || new Tokenizer();
|
||||
for (const prop in pack.tokenizer) {
|
||||
const prevTokenizer = tokenizer[prop];
|
||||
// Replace tokenizer with func to run extension, but fall back if false
|
||||
tokenizer[prop] = (...args) => {
|
||||
let ret = pack.tokenizer[prop].apply(tokenizer, args);
|
||||
if (ret === false) {
|
||||
ret = prevTokenizer.apply(tokenizer, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
opts.tokenizer = tokenizer;
|
||||
}
|
||||
|
||||
// ==-- Parse Hooks extensions --== //
|
||||
if (pack.hooks) {
|
||||
const hooks = marked.defaults.hooks || new Hooks();
|
||||
for (const prop in pack.hooks) {
|
||||
const prevHook = hooks[prop];
|
||||
if (Hooks.passThroughHooks.has(prop)) {
|
||||
hooks[prop] = (arg) => {
|
||||
if (marked.defaults.async) {
|
||||
return Promise.resolve(pack.hooks[prop].call(hooks, arg)).then(ret => {
|
||||
return prevHook.call(hooks, ret);
|
||||
});
|
||||
}
|
||||
|
||||
const ret = pack.hooks[prop].call(hooks, arg);
|
||||
return prevHook.call(hooks, ret);
|
||||
};
|
||||
} else {
|
||||
hooks[prop] = (...args) => {
|
||||
let ret = pack.hooks[prop].apply(hooks, args);
|
||||
if (ret === false) {
|
||||
ret = prevHook.apply(hooks, args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
}
|
||||
opts.hooks = hooks;
|
||||
}
|
||||
|
||||
// ==-- Parse WalkTokens extensions --== //
|
||||
if (pack.walkTokens) {
|
||||
const walkTokens = marked.defaults.walkTokens;
|
||||
opts.walkTokens = function(token) {
|
||||
let values = [];
|
||||
values.push(pack.walkTokens.call(this, token));
|
||||
if (walkTokens) {
|
||||
values = values.concat(walkTokens.call(this, token));
|
||||
}
|
||||
return values;
|
||||
};
|
||||
}
|
||||
|
||||
marked.setOptions(opts);
|
||||
});
|
||||
markedInstance.use(...args);
|
||||
marked.defaults = markedInstance.defaults;
|
||||
changeDefaults(marked.defaults);
|
||||
return marked;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -345,44 +49,14 @@ marked.use = function(...args) {
|
||||
*/
|
||||
|
||||
marked.walkTokens = function(tokens, callback) {
|
||||
let values = [];
|
||||
for (const token of tokens) {
|
||||
values = values.concat(callback.call(marked, token));
|
||||
switch (token.type) {
|
||||
case 'table': {
|
||||
for (const cell of token.header) {
|
||||
values = values.concat(marked.walkTokens(cell.tokens, callback));
|
||||
}
|
||||
for (const row of token.rows) {
|
||||
for (const cell of row) {
|
||||
values = values.concat(marked.walkTokens(cell.tokens, callback));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'list': {
|
||||
values = values.concat(marked.walkTokens(token.items, callback));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (marked.defaults.extensions && marked.defaults.extensions.childTokens && marked.defaults.extensions.childTokens[token.type]) { // Walk any extensions
|
||||
marked.defaults.extensions.childTokens[token.type].forEach(function(childTokens) {
|
||||
values = values.concat(marked.walkTokens(token[childTokens], callback));
|
||||
});
|
||||
} else if (token.tokens) {
|
||||
values = values.concat(marked.walkTokens(token.tokens, callback));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
return markedInstance.walkTokens(tokens, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse Inline
|
||||
* @param {string} src
|
||||
*/
|
||||
marked.parseInline = parseMarkdown(Lexer.lexInline, Parser.parseInline);
|
||||
marked.parseInline = markedInstance.parseInline;
|
||||
|
||||
/**
|
||||
* Expose
|
||||
@ -414,3 +88,4 @@ export { Renderer } from './Renderer.js';
|
||||
export { TextRenderer } from './TextRenderer.js';
|
||||
export { Slugger } from './Slugger.js';
|
||||
export { Hooks } from './Hooks.js';
|
||||
export { Marked } from './Instance.js';
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { marked, setOptions, getDefaults } from '../../src/marked.js';
|
||||
import { Marked, setOptions, getDefaults } from '../../src/marked.js';
|
||||
import { isEqual, firstDiff } from './html-differ.js';
|
||||
import { strictEqual } from 'assert';
|
||||
|
||||
beforeEach(() => {
|
||||
setOptions(getDefaults());
|
||||
setOptions({ silent: true });
|
||||
|
||||
jasmine.addAsyncMatchers({
|
||||
toRender: () => {
|
||||
return {
|
||||
compare: async(spec, expected) => {
|
||||
const marked = new Marked();
|
||||
const result = {};
|
||||
const actual = marked(spec.markdown, spec.options);
|
||||
const actual = marked.parse(spec.markdown, spec.options);
|
||||
result.pass = await isEqual(expected, actual);
|
||||
|
||||
if (result.pass) {
|
||||
@ -41,8 +43,9 @@ beforeEach(() => {
|
||||
},
|
||||
toRenderExact: () => ({
|
||||
compare: async(spec, expected) => {
|
||||
const marked = new Marked();
|
||||
const result = {};
|
||||
const actual = marked(spec.markdown, spec.options);
|
||||
const actual = marked.parse(spec.markdown, spec.options);
|
||||
|
||||
result.pass = strictEqual(expected, actual) === undefined;
|
||||
|
||||
|
@ -55,4 +55,4 @@ runSpecs('CommonMark', './commonmark', true, { gfm: false, pedantic: false, head
|
||||
runSpecs('Original', './original', false, { gfm: false, pedantic: true });
|
||||
runSpecs('New', './new');
|
||||
runSpecs('ReDOS', './redos');
|
||||
runSpecs('Security', './security', false, { silent: true }); // silent - do not show deprecation warning
|
||||
runSpecs('Security', './security');
|
||||
|
117
test/unit/Hooks-spec.js
Normal file
117
test/unit/Hooks-spec.js
Normal file
@ -0,0 +1,117 @@
|
||||
import { marked } from '../../src/marked.js';
|
||||
import { timeout } from './utils.js';
|
||||
|
||||
describe('Hooks', () => {
|
||||
it('should preprocess markdown', () => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
return `# preprocess\n\n${markdown}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = marked('*text*');
|
||||
expect(html.trim()).toBe('<h1 id="preprocess">preprocess</h1>\n<p><em>text</em></p>');
|
||||
});
|
||||
|
||||
it('should preprocess async', async() => {
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
async preprocess(markdown) {
|
||||
await timeout();
|
||||
return `# preprocess async\n\n${markdown}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const promise = marked('*text*');
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
const html = await promise;
|
||||
expect(html.trim()).toBe('<h1 id="preprocess-async">preprocess async</h1>\n<p><em>text</em></p>');
|
||||
});
|
||||
|
||||
it('should preprocess options', () => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
this.options.headerIds = false;
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = marked('# test');
|
||||
expect(html.trim()).toBe('<h1>test</h1>');
|
||||
});
|
||||
|
||||
it('should preprocess options async', async() => {
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
async preprocess(markdown) {
|
||||
await timeout();
|
||||
this.options.headerIds = false;
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = await marked('# test');
|
||||
expect(html.trim()).toBe('<h1>test</h1>');
|
||||
});
|
||||
|
||||
it('should postprocess html', () => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
postprocess(html) {
|
||||
return html + '<h1>postprocess</h1>';
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = marked('*text*');
|
||||
expect(html.trim()).toBe('<p><em>text</em></p>\n<h1>postprocess</h1>');
|
||||
});
|
||||
|
||||
it('should postprocess async', async() => {
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
async postprocess(html) {
|
||||
await timeout();
|
||||
return html + '<h1>postprocess async</h1>\n';
|
||||
}
|
||||
}
|
||||
});
|
||||
const promise = marked('*text*');
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
const html = await promise;
|
||||
expect(html.trim()).toBe('<p><em>text</em></p>\n<h1>postprocess async</h1>');
|
||||
});
|
||||
|
||||
it('should process all hooks in reverse', async() => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
return `# preprocess1\n\n${markdown}`;
|
||||
},
|
||||
postprocess(html) {
|
||||
return html + '<h1>postprocess1</h1>\n';
|
||||
}
|
||||
}
|
||||
});
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
return `# preprocess2\n\n${markdown}`;
|
||||
},
|
||||
async postprocess(html) {
|
||||
await timeout();
|
||||
return html + '<h1>postprocess2 async</h1>\n';
|
||||
}
|
||||
}
|
||||
});
|
||||
const promise = marked('*text*');
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
const html = await promise;
|
||||
expect(html.trim()).toBe('<h1 id="preprocess1">preprocess1</h1>\n<h1 id="preprocess2">preprocess2</h1>\n<p><em>text</em></p>\n<h1>postprocess2 async</h1>\n<h1>postprocess1</h1>');
|
||||
});
|
||||
});
|
74
test/unit/Slugger-spec.js
Normal file
74
test/unit/Slugger-spec.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { Slugger } from '../../src/Slugger.js';
|
||||
|
||||
describe('Test slugger functionality', () => {
|
||||
it('should use lowercase slug', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('Test')).toBe('test');
|
||||
});
|
||||
|
||||
it('should be unique to avoid collisions 1280', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('test')).toBe('test');
|
||||
expect(slugger.slug('test')).toBe('test-1');
|
||||
expect(slugger.slug('test')).toBe('test-2');
|
||||
});
|
||||
|
||||
it('should be unique when slug ends with number', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('test 1')).toBe('test-1');
|
||||
expect(slugger.slug('test')).toBe('test');
|
||||
expect(slugger.slug('test')).toBe('test-2');
|
||||
});
|
||||
|
||||
it('should be unique when slug ends with hyphen number', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('foo')).toBe('foo');
|
||||
expect(slugger.slug('foo')).toBe('foo-1');
|
||||
expect(slugger.slug('foo 1')).toBe('foo-1-1');
|
||||
expect(slugger.slug('foo-1')).toBe('foo-1-2');
|
||||
expect(slugger.slug('foo')).toBe('foo-2');
|
||||
});
|
||||
|
||||
it('should allow non-latin chars', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('привет')).toBe('привет');
|
||||
});
|
||||
|
||||
it('should remove ampersands 857', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('This & That Section')).toBe('this--that-section');
|
||||
});
|
||||
|
||||
it('should remove periods', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('file.txt')).toBe('filetxt');
|
||||
});
|
||||
|
||||
it('should remove html tags', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('<em>html</em>')).toBe('html');
|
||||
});
|
||||
|
||||
it('should not increment seen when using dryrun option', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('<h1>This Section</h1>', { dryrun: true })).toBe('this-section');
|
||||
expect(slugger.slug('<h1>This Section</h1>')).toBe('this-section');
|
||||
});
|
||||
|
||||
it('should still return the next unique id when using dryrun', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('<h1>This Section</h1>')).toBe('this-section');
|
||||
expect(slugger.slug('<h1>This Section</h1>', { dryrun: true })).toBe('this-section-1');
|
||||
});
|
||||
|
||||
it('should be repeatable in a sequence', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('foo')).toBe('foo');
|
||||
expect(slugger.slug('foo')).toBe('foo-1');
|
||||
expect(slugger.slug('foo')).toBe('foo-2');
|
||||
expect(slugger.slug('foo', { dryrun: true })).toBe('foo-3');
|
||||
expect(slugger.slug('foo', { dryrun: true })).toBe('foo-3');
|
||||
expect(slugger.slug('foo')).toBe('foo-3');
|
||||
expect(slugger.slug('foo')).toBe('foo-4');
|
||||
});
|
||||
});
|
75
test/unit/instance-spec.js
Normal file
75
test/unit/instance-spec.js
Normal file
@ -0,0 +1,75 @@
|
||||
import { marked, Marked, Renderer } from '../../src/marked.js';
|
||||
|
||||
describe('Marked', () => {
|
||||
it('should allow multiple instances', () => {
|
||||
const marked1 = new Marked({
|
||||
silent: true,
|
||||
renderer: {
|
||||
heading() {
|
||||
return 'im marked1';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const marked2 = new Marked({
|
||||
silent: true,
|
||||
renderer: {
|
||||
heading() {
|
||||
return 'im marked2';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(marked1.parse('# header')).toBe('im marked1');
|
||||
expect(marked2.parse('# header')).toBe('im marked2');
|
||||
expect(marked.parse('# header')).toBe('<h1 id="header">header</h1>\n');
|
||||
});
|
||||
|
||||
it('should work with use', () => {
|
||||
const marked1 = new Marked();
|
||||
marked1.use({
|
||||
silent: true,
|
||||
renderer: {
|
||||
heading() {
|
||||
return 'im marked1';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const marked2 = new Marked();
|
||||
marked2.use({
|
||||
silent: true,
|
||||
renderer: {
|
||||
heading() {
|
||||
return 'im marked2';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(marked1.parse('# header')).toBe('im marked1');
|
||||
expect(marked2.parse('# header')).toBe('im marked2');
|
||||
expect(marked.parse('# header')).toBe('<h1 id="header">header</h1>\n');
|
||||
});
|
||||
|
||||
it('should work with setOptions', () => {
|
||||
const marked1 = new Marked();
|
||||
const marked1Renderer = new Renderer();
|
||||
marked1Renderer.heading = () => 'im marked1';
|
||||
marked1.setOptions({
|
||||
silent: true,
|
||||
renderer: marked1Renderer
|
||||
});
|
||||
|
||||
const marked2 = new Marked();
|
||||
const marked2Renderer = new Renderer();
|
||||
marked2Renderer.heading = () => 'im marked2';
|
||||
marked2.setOptions({
|
||||
silent: true,
|
||||
renderer: marked2Renderer
|
||||
});
|
||||
|
||||
expect(marked1.parse('# header')).toBe('im marked1');
|
||||
expect(marked2.parse('# header')).toBe('im marked2');
|
||||
expect(marked.parse('# header')).toBe('<h1 id="header">header</h1>\n');
|
||||
});
|
||||
});
|
@ -1,14 +1,9 @@
|
||||
import { marked, Renderer, Slugger, lexer, parseInline, use, getDefaults, walkTokens as _walkTokens, defaults, setOptions } from '../../src/marked.js';
|
||||
|
||||
async function timeout(ms = 1) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
import { timeout } from './utils.js';
|
||||
|
||||
describe('Test heading ID functionality', () => {
|
||||
it('should add id attribute by default', () => {
|
||||
const renderer = new Renderer();
|
||||
it('should add id attribute', () => {
|
||||
const renderer = new Renderer({ ...defaults, headerIds: true });
|
||||
const slugger = new Slugger();
|
||||
const header = renderer.heading('test', 1, 'test', slugger);
|
||||
expect(header).toBe('<h1 id="test">test</h1>\n');
|
||||
@ -21,79 +16,6 @@ describe('Test heading ID functionality', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test slugger functionality', () => {
|
||||
it('should use lowercase slug', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('Test')).toBe('test');
|
||||
});
|
||||
|
||||
it('should be unique to avoid collisions 1280', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('test')).toBe('test');
|
||||
expect(slugger.slug('test')).toBe('test-1');
|
||||
expect(slugger.slug('test')).toBe('test-2');
|
||||
});
|
||||
|
||||
it('should be unique when slug ends with number', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('test 1')).toBe('test-1');
|
||||
expect(slugger.slug('test')).toBe('test');
|
||||
expect(slugger.slug('test')).toBe('test-2');
|
||||
});
|
||||
|
||||
it('should be unique when slug ends with hyphen number', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('foo')).toBe('foo');
|
||||
expect(slugger.slug('foo')).toBe('foo-1');
|
||||
expect(slugger.slug('foo 1')).toBe('foo-1-1');
|
||||
expect(slugger.slug('foo-1')).toBe('foo-1-2');
|
||||
expect(slugger.slug('foo')).toBe('foo-2');
|
||||
});
|
||||
|
||||
it('should allow non-latin chars', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('привет')).toBe('привет');
|
||||
});
|
||||
|
||||
it('should remove ampersands 857', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('This & That Section')).toBe('this--that-section');
|
||||
});
|
||||
|
||||
it('should remove periods', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('file.txt')).toBe('filetxt');
|
||||
});
|
||||
|
||||
it('should remove html tags', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('<em>html</em>')).toBe('html');
|
||||
});
|
||||
|
||||
it('should not increment seen when using dryrun option', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('<h1>This Section</h1>', { dryrun: true })).toBe('this-section');
|
||||
expect(slugger.slug('<h1>This Section</h1>')).toBe('this-section');
|
||||
});
|
||||
|
||||
it('should still return the next unique id when using dryrun', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('<h1>This Section</h1>')).toBe('this-section');
|
||||
expect(slugger.slug('<h1>This Section</h1>', { dryrun: true })).toBe('this-section-1');
|
||||
});
|
||||
|
||||
it('should be repeatable in a sequence', () => {
|
||||
const slugger = new Slugger();
|
||||
expect(slugger.slug('foo')).toBe('foo');
|
||||
expect(slugger.slug('foo')).toBe('foo-1');
|
||||
expect(slugger.slug('foo')).toBe('foo-2');
|
||||
expect(slugger.slug('foo', { dryrun: true })).toBe('foo-3');
|
||||
expect(slugger.slug('foo', { dryrun: true })).toBe('foo-3');
|
||||
expect(slugger.slug('foo')).toBe('foo-3');
|
||||
expect(slugger.slug('foo')).toBe('foo-4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test paragraph token type', () => {
|
||||
it('should use the "paragraph" type on top level', () => {
|
||||
const md = 'A Paragraph.\n\n> A blockquote\n\n- list item\n';
|
||||
@ -707,7 +629,7 @@ used extension2 walked</p>
|
||||
const html = marked('This is a *paragraph* with blue text. {blue}\n'
|
||||
+ '# This is a *header* with red text {red}');
|
||||
expect(html).toBe('<p style="color:blue;">This is a <em>paragraph</em> with blue text.</p>\n'
|
||||
+ '<h1 style="color:red;">This is a <em>header</em> with red text</h1>\n');
|
||||
+ '<h1 style="color:red;">This is a <em>header</em> with red text</h1>\n');
|
||||
});
|
||||
|
||||
it('should use renderer', () => {
|
||||
@ -1128,118 +1050,3 @@ br
|
||||
expect(html.trim()).toBe('<p><em>text</em></p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hooks', () => {
|
||||
it('should preprocess markdown', () => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
return `# preprocess\n\n${markdown}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = marked('*text*');
|
||||
expect(html.trim()).toBe('<h1 id="preprocess">preprocess</h1>\n<p><em>text</em></p>');
|
||||
});
|
||||
|
||||
it('should preprocess async', async() => {
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
async preprocess(markdown) {
|
||||
await timeout();
|
||||
return `# preprocess async\n\n${markdown}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const promise = marked('*text*');
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
const html = await promise;
|
||||
expect(html.trim()).toBe('<h1 id="preprocess-async">preprocess async</h1>\n<p><em>text</em></p>');
|
||||
});
|
||||
|
||||
it('should preprocess options', () => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
this.options.headerIds = false;
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = marked('# test');
|
||||
expect(html.trim()).toBe('<h1>test</h1>');
|
||||
});
|
||||
|
||||
it('should preprocess options async', async() => {
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
async preprocess(markdown) {
|
||||
await timeout();
|
||||
this.options.headerIds = false;
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = await marked('# test');
|
||||
expect(html.trim()).toBe('<h1>test</h1>');
|
||||
});
|
||||
|
||||
it('should postprocess html', () => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
postprocess(html) {
|
||||
return html + '<h1>postprocess</h1>';
|
||||
}
|
||||
}
|
||||
});
|
||||
const html = marked('*text*');
|
||||
expect(html.trim()).toBe('<p><em>text</em></p>\n<h1>postprocess</h1>');
|
||||
});
|
||||
|
||||
it('should postprocess async', async() => {
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
async postprocess(html) {
|
||||
await timeout();
|
||||
return html + '<h1>postprocess async</h1>\n';
|
||||
}
|
||||
}
|
||||
});
|
||||
const promise = marked('*text*');
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
const html = await promise;
|
||||
expect(html.trim()).toBe('<p><em>text</em></p>\n<h1>postprocess async</h1>');
|
||||
});
|
||||
|
||||
it('should process all hooks in reverse', async() => {
|
||||
marked.use({
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
return `# preprocess1\n\n${markdown}`;
|
||||
},
|
||||
postprocess(html) {
|
||||
return html + '<h1>postprocess1</h1>\n';
|
||||
}
|
||||
}
|
||||
});
|
||||
marked.use({
|
||||
async: true,
|
||||
hooks: {
|
||||
preprocess(markdown) {
|
||||
return `# preprocess2\n\n${markdown}`;
|
||||
},
|
||||
async postprocess(html) {
|
||||
await timeout();
|
||||
return html + '<h1>postprocess2 async</h1>\n';
|
||||
}
|
||||
}
|
||||
});
|
||||
const promise = marked('*text*');
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
const html = await promise;
|
||||
expect(html.trim()).toBe('<h1 id="preprocess1">preprocess1</h1>\n<h1 id="preprocess2">preprocess2</h1>\n<p><em>text</em></p>\n<h1>postprocess2 async</h1>\n<h1>postprocess1</h1>');
|
||||
});
|
||||
});
|
||||
|
5
test/unit/utils.js
Normal file
5
test/unit/utils.js
Normal file
@ -0,0 +1,5 @@
|
||||
export async function timeout(ms = 1) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user