diff --git a/.gitignore b/.gitignore index 6dedd8e232..dc62331aa8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ docs/LICENSE.md vuln.js man/marked.1 marked.min.js +test.js diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index b2c20718d6..6c7c2922c3 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -261,6 +261,8 @@ Hooks are methods that hook into some part of marked. The following hooks are av | `preprocess(markdown: string): string` | Process markdown before sending it to marked. | | `postprocess(html: string): string` | Process html after marked has finished parsing. | | `processAllTokens(tokens: Token[]): Token[]` | Process all tokens before walk tokens. | +| `provideLexer(): (src: string, options?: MarkedOptions) => Token[]` | Provide function to tokenize markdown. | +| `provideParser(): (tokens: Token[], options?: MarkedOptions) => string` | Provide function to parse tokens. | `marked.use()` can be called multiple times with different `hooks` functions. Each function will be called in order, starting with the function that was assigned *last*. @@ -325,6 +327,45 @@ console.log(marked.parse(` ``` +**Example:** Save reflinks for chunked rendering + +```js +import { marked, Lexer } from 'marked'; + +let refLinks = {}; + +// Override function +function processAllTokens(tokens) { + refLinks = tokens.links; + return tokens; +} + +function provideLexer(src, options) { + return (src, options) => { + const lexer = new Lexer(options); + lexer.tokens.links = refLinks; + return this.block ? lexer.lex(src) : lexer.inlineTokens(src); + }; +} + +marked.use({ hooks: { processAllTokens, provideLexer } }); + +// Parse reflinks separately from markdown that uses them +marked.parse(` +[test]: http://example.com +`); + +console.log(marked.parse(` +[test link][test] +`)); +``` + +**Output:** + +```html +

test link

+``` + ***

Custom Extensions : extensions

diff --git a/src/Hooks.ts b/src/Hooks.ts index 967a05e8f0..8d84b89c0d 100644 --- a/src/Hooks.ts +++ b/src/Hooks.ts @@ -1,9 +1,12 @@ import { _defaults } from './defaults.ts'; +import { _Lexer } from './Lexer.ts'; +import { _Parser } from './Parser.ts'; import type { MarkedOptions } from './MarkedOptions.ts'; import type { Token, TokensList } from './Tokens.ts'; export class _Hooks { options: MarkedOptions; + block: boolean | undefined; constructor(options?: MarkedOptions) { this.options = options || _defaults; @@ -35,4 +38,18 @@ export class _Hooks { processAllTokens(tokens: Token[] | TokensList) { return tokens; } + + /** + * Provide function to tokenize markdown + */ + provideLexer() { + return this.block ? _Lexer.lex : _Lexer.lexInline; + } + + /** + * Provide function to parse tokens + */ + provideParser() { + return this.block ? _Parser.parse : _Parser.parseInline; + } } diff --git a/src/Instance.ts b/src/Instance.ts index 758a21d7d9..abadb87230 100644 --- a/src/Instance.ts +++ b/src/Instance.ts @@ -18,8 +18,8 @@ export class Marked { defaults = _getDefaults(); options = this.setOptions; - parse = this.parseMarkdown(_Lexer.lex, _Parser.parse); - parseInline = this.parseMarkdown(_Lexer.lexInline, _Parser.parseInline); + parse = this.parseMarkdown(true); + parseInline = this.parseMarkdown(false); Parser = _Parser; Renderer = _Renderer; @@ -195,11 +195,11 @@ export class Marked { if (!(prop in hooks)) { throw new Error(`hook '${prop}' does not exist`); } - if (prop === 'options') { - // ignore options property + if (['options', 'block'].includes(prop)) { + // ignore options and block properties continue; } - const hooksProp = prop as Exclude; + const hooksProp = prop as Exclude; const hooksFunc = pack.hooks[hooksProp] as UnknownFunction; const prevHook = hooks[hooksProp] as UnknownFunction; if (_Hooks.passThroughHooks.has(prop)) { @@ -261,7 +261,7 @@ export class Marked { return _Parser.parse(tokens, options ?? this.defaults); } - private parseMarkdown(lexer: (src: string, options?: MarkedOptions) => TokensList | Token[], parser: (tokens: Token[], options?: MarkedOptions) => string) { + private parseMarkdown(blockType: boolean) { type overloadedParse = { (src: string, options: MarkedOptions & { async: true }): Promise; (src: string, options: MarkedOptions & { async: false }): string; @@ -291,8 +291,12 @@ export class Marked { if (opt.hooks) { opt.hooks.options = opt; + opt.hooks.block = blockType; } + const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); + const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); + if (opt.async) { return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) .then(src => lexer(src, opt)) @@ -309,7 +313,7 @@ export class Marked { } let tokens = lexer(src, opt); if (opt.hooks) { - tokens = opt.hooks.processAllTokens(tokens) as Token[] | TokensList; + tokens = opt.hooks.processAllTokens(tokens); } if (opt.walkTokens) { this.walkTokens(tokens, opt.walkTokens); diff --git a/src/MarkedOptions.ts b/src/MarkedOptions.ts index 9c4c1fe7cb..a1bf485d7a 100644 --- a/src/MarkedOptions.ts +++ b/src/MarkedOptions.ts @@ -34,7 +34,7 @@ export interface RendererExtension { export type TokenizerAndRendererExtension = TokenizerExtension | RendererExtension | (TokenizerExtension & RendererExtension); -type HooksApi = Omit<_Hooks, 'constructor' | 'options'>; +type HooksApi = Omit<_Hooks, 'constructor' | 'options' | 'block'>; type HooksObject = { [K in keyof HooksApi]?: (this: _Hooks, ...args: Parameters) => ReturnType | Promise> }; @@ -77,6 +77,8 @@ export interface MarkedExtension { * preprocess is called to process markdown before sending it to marked. * processAllTokens is called with the TokensList before walkTokens. * postprocess is called to process html after marked has finished parsing. + * provideLexer is called to provide a function to tokenize markdown. + * provideParser is called to provide a function to parse tokens. */ hooks?: HooksObject | undefined | null; diff --git a/test/types/marked.ts b/test/types/marked.ts index bf7a3e9433..b8caa9ab50 100644 --- a/test/types/marked.ts +++ b/test/types/marked.ts @@ -346,6 +346,16 @@ marked.use({ } } }); +marked.use({ + hooks: { + provideLexer() { + return this.block ? Lexer.lex : Lexer.lexInline; + }, + provideParser() { + return this.block ? Parser.parse : Parser.parseInline; + }, + } +}); marked.use({ async: true, hooks: { diff --git a/test/unit/Hooks.test.js b/test/unit/Hooks.test.js index 8907633069..83f7676115 100644 --- a/test/unit/Hooks.test.js +++ b/test/unit/Hooks.test.js @@ -190,4 +190,72 @@ describe('Hooks', () => {

postprocess2 async

postprocess1

`); }); + + it('should provide lexer', () => { + marked.use({ + hooks: { + provideLexer() { + return (src) => [createHeadingToken(src)]; + }, + }, + }); + const html = marked.parse('text'); + assert.strictEqual(html.trim(), '

text

'); + }); + + it('should provide lexer async', async() => { + marked.use({ + async: true, + hooks: { + provideLexer() { + return async(src) => { + await timeout(); + return [createHeadingToken(src)]; + }; + }, + }, + }); + const html = await marked.parse('text'); + assert.strictEqual(html.trim(), '

text

'); + }); + + it('should provide parser return object', () => { + marked.use({ + hooks: { + provideParser() { + return (tokens) => ({ text: 'test parser' }); + }, + }, + }); + const html = marked.parse('text'); + assert.strictEqual(html.text, 'test parser'); + }); + + it('should provide parser', () => { + marked.use({ + hooks: { + provideParser() { + return (tokens) => 'test parser'; + }, + }, + }); + const html = marked.parse('text'); + assert.strictEqual(html.trim(), 'test parser'); + }); + + it('should provide parser async', async() => { + marked.use({ + async: true, + hooks: { + provideParser() { + return async(tokens) => { + await timeout(); + return 'test parser'; + }; + }, + }, + }); + const html = await marked.parse('text'); + assert.strictEqual(html.trim(), 'test parser'); + }); });