2023-08-09 23:33:46 -06:00
|
|
|
import { _getDefaults } from './defaults.ts';
|
2023-07-29 08:31:34 +02:00
|
|
|
import { _Lexer } from './Lexer.ts';
|
|
|
|
import { _Parser } from './Parser.ts';
|
|
|
|
import { _Hooks } from './Hooks.ts';
|
|
|
|
import { _Renderer } from './Renderer.ts';
|
|
|
|
import { _Tokenizer } from './Tokenizer.ts';
|
|
|
|
import { _TextRenderer } from './TextRenderer.ts';
|
|
|
|
import { _Slugger } from './Slugger.ts';
|
2023-06-09 22:10:12 -05:00
|
|
|
import {
|
|
|
|
checkDeprecations,
|
|
|
|
escape
|
2023-07-29 08:31:34 +02:00
|
|
|
} from './helpers.ts';
|
|
|
|
import type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';
|
2023-08-19 16:55:56 -06:00
|
|
|
import type { Token, Tokens, TokensList } from './Tokens.ts';
|
2023-07-29 08:31:34 +02:00
|
|
|
|
|
|
|
export type ResultCallback = (error: Error | null, parseResult?: string) => undefined | void;
|
2023-08-26 09:57:05 -06:00
|
|
|
export type MaybePromise = void | Promise<void>;
|
2023-06-09 22:10:12 -05:00
|
|
|
|
2023-08-09 23:33:46 -06:00
|
|
|
type UnknownFunction = (...args: unknown[]) => unknown;
|
|
|
|
type GenericRendererFunction = (...args: unknown[]) => string | false;
|
|
|
|
|
2023-06-09 22:10:12 -05:00
|
|
|
export class Marked {
|
2023-07-29 08:31:34 +02:00
|
|
|
defaults = _getDefaults();
|
2023-06-09 22:10:12 -05:00
|
|
|
options = this.setOptions;
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
parse = this.#parseMarkdown(_Lexer.lex, _Parser.parse);
|
|
|
|
parseInline = this.#parseMarkdown(_Lexer.lexInline, _Parser.parseInline);
|
2023-06-09 22:10:12 -05:00
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
Parser = _Parser;
|
|
|
|
parser = _Parser.parse;
|
|
|
|
Renderer = _Renderer;
|
|
|
|
TextRenderer = _TextRenderer;
|
|
|
|
Lexer = _Lexer;
|
|
|
|
lexer = _Lexer.lex;
|
|
|
|
Tokenizer = _Tokenizer;
|
|
|
|
Slugger = _Slugger;
|
|
|
|
Hooks = _Hooks;
|
2023-06-09 22:10:12 -05:00
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
constructor(...args: MarkedExtension[]) {
|
2023-06-09 22:10:12 -05:00
|
|
|
this.use(...args);
|
|
|
|
}
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
/**
|
|
|
|
* Run callback for every token
|
|
|
|
*/
|
2023-08-26 09:57:05 -06:00
|
|
|
walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {
|
|
|
|
let values: MaybePromise[] = [];
|
2023-06-09 22:10:12 -05:00
|
|
|
for (const token of tokens) {
|
|
|
|
values = values.concat(callback.call(this, token));
|
|
|
|
switch (token.type) {
|
|
|
|
case 'table': {
|
2023-08-19 16:55:56 -06:00
|
|
|
const tableToken = token as Tokens.Table;
|
|
|
|
for (const cell of tableToken.header) {
|
|
|
|
values = values.concat(this.walkTokens(cell.tokens, callback));
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
2023-08-19 16:55:56 -06:00
|
|
|
for (const row of tableToken.rows) {
|
2023-06-09 22:10:12 -05:00
|
|
|
for (const cell of row) {
|
2023-08-19 16:55:56 -06:00
|
|
|
values = values.concat(this.walkTokens(cell.tokens, callback));
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'list': {
|
2023-08-19 16:55:56 -06:00
|
|
|
const listToken = token as Tokens.List;
|
|
|
|
values = values.concat(this.walkTokens(listToken.items, callback));
|
2023-06-09 22:10:12 -05:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
2023-08-19 16:55:56 -06:00
|
|
|
const genericToken = token as Tokens.Generic;
|
|
|
|
if (this.defaults.extensions?.childTokens?.[genericToken.type]) {
|
|
|
|
this.defaults.extensions.childTokens[genericToken.type].forEach((childTokens) => {
|
|
|
|
values = values.concat(this.walkTokens(genericToken[childTokens], callback));
|
2023-06-09 22:10:12 -05:00
|
|
|
});
|
2023-08-19 16:55:56 -06:00
|
|
|
} else if (genericToken.tokens) {
|
|
|
|
values = values.concat(this.walkTokens(genericToken.tokens, callback));
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return values;
|
|
|
|
}
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
use(...args: MarkedExtension[]) {
|
2023-08-19 16:55:56 -06:00
|
|
|
const extensions: MarkedOptions['extensions'] = this.defaults.extensions || { renderers: {}, childTokens: {} };
|
2023-06-09 22:10:12 -05:00
|
|
|
|
|
|
|
args.forEach((pack) => {
|
|
|
|
// copy options to new object
|
2023-07-29 08:31:34 +02:00
|
|
|
const opts = { ...pack } as MarkedOptions;
|
2023-06-09 22:10:12 -05:00
|
|
|
|
|
|
|
// 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');
|
|
|
|
}
|
2023-07-29 08:31:34 +02:00
|
|
|
if ('renderer' in ext) { // Renderer extensions
|
2023-06-09 22:10:12 -05:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2023-07-29 08:31:34 +02:00
|
|
|
if ('tokenizer' in ext) { // Tokenizer Extensions
|
2023-06-09 22:10:12 -05:00
|
|
|
if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {
|
|
|
|
throw new Error("extension level must be 'block' or 'inline'");
|
|
|
|
}
|
2023-08-19 16:55:56 -06:00
|
|
|
const extLevel = extensions[ext.level];
|
|
|
|
if (extLevel) {
|
|
|
|
extLevel.unshift(ext.tokenizer);
|
2023-06-09 22:10:12 -05:00
|
|
|
} else {
|
|
|
|
extensions[ext.level] = [ext.tokenizer];
|
|
|
|
}
|
|
|
|
if (ext.start) { // Function to check for start of token
|
|
|
|
if (ext.level === 'block') {
|
|
|
|
if (extensions.startBlock) {
|
2023-08-19 16:55:56 -06:00
|
|
|
extensions.startBlock.push(ext.start);
|
2023-06-09 22:10:12 -05:00
|
|
|
} else {
|
2023-08-19 16:55:56 -06:00
|
|
|
extensions.startBlock = [ext.start];
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
} else if (ext.level === 'inline') {
|
|
|
|
if (extensions.startInline) {
|
2023-08-19 16:55:56 -06:00
|
|
|
extensions.startInline.push(ext.start);
|
2023-06-09 22:10:12 -05:00
|
|
|
} else {
|
2023-08-19 16:55:56 -06:00
|
|
|
extensions.startInline = [ext.start];
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-07-29 08:31:34 +02:00
|
|
|
if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens
|
2023-06-09 22:10:12 -05:00
|
|
|
extensions.childTokens[ext.name] = ext.childTokens;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
opts.extensions = extensions;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ==-- Parse "overwrite" extensions --== //
|
|
|
|
if (pack.renderer) {
|
2023-07-29 08:31:34 +02:00
|
|
|
const renderer = this.defaults.renderer || new _Renderer(this.defaults);
|
2023-06-09 22:10:12 -05:00
|
|
|
for (const prop in pack.renderer) {
|
2023-08-09 23:33:46 -06:00
|
|
|
const rendererFunc = pack.renderer[prop as keyof MarkedExtension['renderer']] as GenericRendererFunction;
|
|
|
|
const rendererKey = prop as keyof _Renderer;
|
|
|
|
const prevRenderer = renderer[rendererKey] as GenericRendererFunction;
|
2023-06-09 22:10:12 -05:00
|
|
|
// Replace renderer with func to run extension, but fall back if false
|
2023-08-09 23:33:46 -06:00
|
|
|
renderer[rendererKey] = (...args: unknown[]) => {
|
|
|
|
let ret = rendererFunc.apply(renderer, args);
|
2023-06-09 22:10:12 -05:00
|
|
|
if (ret === false) {
|
|
|
|
ret = prevRenderer.apply(renderer, args);
|
|
|
|
}
|
2023-08-09 23:33:46 -06:00
|
|
|
return ret || '';
|
2023-06-09 22:10:12 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
opts.renderer = renderer;
|
|
|
|
}
|
|
|
|
if (pack.tokenizer) {
|
2023-07-29 08:31:34 +02:00
|
|
|
const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults);
|
2023-06-09 22:10:12 -05:00
|
|
|
for (const prop in pack.tokenizer) {
|
2023-08-09 23:33:46 -06:00
|
|
|
const tokenizerFunc = pack.tokenizer[prop as keyof MarkedExtension['tokenizer']] as UnknownFunction;
|
|
|
|
const tokenizerKey = prop as keyof _Tokenizer;
|
|
|
|
const prevTokenizer = tokenizer[tokenizerKey] as UnknownFunction;
|
2023-06-09 22:10:12 -05:00
|
|
|
// Replace tokenizer with func to run extension, but fall back if false
|
2023-08-09 23:33:46 -06:00
|
|
|
tokenizer[tokenizerKey] = (...args: unknown[]) => {
|
|
|
|
let ret = tokenizerFunc.apply(tokenizer, args);
|
2023-06-09 22:10:12 -05:00
|
|
|
if (ret === false) {
|
|
|
|
ret = prevTokenizer.apply(tokenizer, args);
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
opts.tokenizer = tokenizer;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ==-- Parse Hooks extensions --== //
|
|
|
|
if (pack.hooks) {
|
2023-07-29 08:31:34 +02:00
|
|
|
const hooks = this.defaults.hooks || new _Hooks();
|
2023-06-09 22:10:12 -05:00
|
|
|
for (const prop in pack.hooks) {
|
2023-08-09 23:33:46 -06:00
|
|
|
const hooksFunc = pack.hooks[prop as keyof MarkedExtension['hooks']] as UnknownFunction;
|
|
|
|
const hooksKey = prop as keyof _Hooks;
|
|
|
|
const prevHook = hooks[hooksKey] as UnknownFunction;
|
2023-07-29 08:31:34 +02:00
|
|
|
if (_Hooks.passThroughHooks.has(prop)) {
|
2023-08-09 23:33:46 -06:00
|
|
|
hooks[hooksKey as 'preprocess' | 'postprocess'] = (arg: string | undefined) => {
|
2023-06-09 22:10:12 -05:00
|
|
|
if (this.defaults.async) {
|
2023-08-09 23:33:46 -06:00
|
|
|
return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => {
|
|
|
|
return prevHook.call(hooks, ret) as string;
|
2023-06-09 22:10:12 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-08-09 23:33:46 -06:00
|
|
|
const ret = hooksFunc.call(hooks, arg);
|
|
|
|
return prevHook.call(hooks, ret) as string;
|
2023-06-09 22:10:12 -05:00
|
|
|
};
|
|
|
|
} else {
|
2023-08-09 23:33:46 -06:00
|
|
|
hooks[hooksKey] = (...args: unknown[]) => {
|
|
|
|
let ret = hooksFunc.apply(hooks, args);
|
2023-06-09 22:10:12 -05:00
|
|
|
if (ret === false) {
|
|
|
|
ret = prevHook.apply(hooks, args);
|
|
|
|
}
|
2023-08-09 23:33:46 -06:00
|
|
|
return ret as string;
|
2023-06-09 22:10:12 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
opts.hooks = hooks;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ==-- Parse WalkTokens extensions --== //
|
|
|
|
if (pack.walkTokens) {
|
|
|
|
const walkTokens = this.defaults.walkTokens;
|
2023-08-19 16:55:56 -06:00
|
|
|
const packWalktokens = pack.walkTokens;
|
2023-06-09 22:10:12 -05:00
|
|
|
opts.walkTokens = function(token) {
|
2023-08-26 09:57:05 -06:00
|
|
|
let values: MaybePromise[] = [];
|
2023-08-19 16:55:56 -06:00
|
|
|
values.push(packWalktokens.call(this, token));
|
2023-06-09 22:10:12 -05:00
|
|
|
if (walkTokens) {
|
|
|
|
values = values.concat(walkTokens.call(this, token));
|
|
|
|
}
|
|
|
|
return values;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
this.defaults = { ...this.defaults, ...opts };
|
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2023-08-09 23:33:46 -06:00
|
|
|
setOptions(opt: MarkedOptions) {
|
2023-06-09 22:10:12 -05:00
|
|
|
this.defaults = { ...this.defaults, ...opt };
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2023-08-09 23:33:46 -06:00
|
|
|
#parseMarkdown(lexer: (src: string, options?: MarkedOptions) => TokensList | Token[], parser: (tokens: Token[], options?: MarkedOptions) => string) {
|
2023-07-29 08:31:34 +02:00
|
|
|
return (src: string, optOrCallback?: MarkedOptions | ResultCallback | undefined | null, callback?: ResultCallback | undefined): string | Promise<string | undefined> | undefined => {
|
|
|
|
if (typeof optOrCallback === 'function') {
|
|
|
|
callback = optOrCallback;
|
|
|
|
optOrCallback = null;
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
const origOpt = { ...optOrCallback };
|
|
|
|
const opt = { ...this.defaults, ...origOpt };
|
2023-08-19 16:58:32 -06:00
|
|
|
|
|
|
|
// Show warning if an extension set async to true but the parse was called with async: false
|
|
|
|
if (this.defaults.async === true && origOpt.async === false) {
|
|
|
|
if (!opt.silent) {
|
|
|
|
console.warn('marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored.');
|
|
|
|
}
|
|
|
|
|
|
|
|
opt.async = true;
|
|
|
|
}
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
const throwError = this.#onError(!!opt.silent, !!opt.async, callback);
|
2023-06-09 22:10:12 -05:00
|
|
|
|
|
|
|
// 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) {
|
2023-08-19 16:55:56 -06:00
|
|
|
const resultCallback = callback;
|
2023-06-09 22:10:12 -05:00
|
|
|
const highlight = opt.highlight;
|
2023-07-29 08:31:34 +02:00
|
|
|
let tokens: TokensList | Token[];
|
2023-06-09 22:10:12 -05:00
|
|
|
|
|
|
|
try {
|
|
|
|
if (opt.hooks) {
|
2023-08-09 23:33:46 -06:00
|
|
|
src = opt.hooks.preprocess(src) as string;
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
tokens = lexer(src, opt);
|
|
|
|
} catch (e) {
|
2023-07-29 08:31:34 +02:00
|
|
|
return throwError(e as Error);
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
const done = (err?: Error) => {
|
2023-06-09 22:10:12 -05:00
|
|
|
let out;
|
|
|
|
|
|
|
|
if (!err) {
|
|
|
|
try {
|
|
|
|
if (opt.walkTokens) {
|
|
|
|
this.walkTokens(tokens, opt.walkTokens);
|
|
|
|
}
|
2023-08-19 16:55:56 -06:00
|
|
|
out = parser(tokens, opt);
|
2023-06-09 22:10:12 -05:00
|
|
|
if (opt.hooks) {
|
2023-08-09 23:33:46 -06:00
|
|
|
out = opt.hooks.postprocess(out) as string;
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2023-07-29 08:31:34 +02:00
|
|
|
err = e as Error;
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
opt.highlight = highlight;
|
|
|
|
|
|
|
|
return err
|
|
|
|
? throwError(err)
|
2023-08-19 16:55:56 -06:00
|
|
|
: resultCallback(null, out) as undefined;
|
2023-06-09 22:10:12 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
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) {
|
2023-08-09 23:33:46 -06:00
|
|
|
src = opt.hooks.preprocess(src) as string;
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
const tokens = lexer(src, opt);
|
|
|
|
if (opt.walkTokens) {
|
|
|
|
this.walkTokens(tokens, opt.walkTokens);
|
|
|
|
}
|
|
|
|
let html = parser(tokens, opt);
|
|
|
|
if (opt.hooks) {
|
2023-08-09 23:33:46 -06:00
|
|
|
html = opt.hooks.postprocess(html) as string;
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
return html;
|
|
|
|
} catch (e) {
|
2023-07-29 08:31:34 +02:00
|
|
|
return throwError(e as Error);
|
2023-06-09 22:10:12 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-07-29 08:31:34 +02:00
|
|
|
#onError(silent: boolean, async: boolean, callback?: ResultCallback) {
|
|
|
|
return (e: Error): string | Promise<string> | undefined => {
|
2023-07-07 16:40:27 +02:00
|
|
|
e.message += '\nPlease report this to https://github.com/markedjs/marked.';
|
2023-06-09 22:10:12 -05:00
|
|
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|