Skip to content

Commit 447f5af

Browse files
authoredAug 26, 2024··
feat: add provideLexer and provideParser hooks (#3424)
1 parent 0076503 commit 447f5af

File tree

7 files changed

+151
-8
lines changed

7 files changed

+151
-8
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ docs/LICENSE.md
99
vuln.js
1010
man/marked.1
1111
marked.min.js
12+
test.js

‎docs/USING_PRO.md

+41
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ Hooks are methods that hook into some part of marked. The following hooks are av
261261
| `preprocess(markdown: string): string` | Process markdown before sending it to marked. |
262262
| `postprocess(html: string): string` | Process html after marked has finished parsing. |
263263
| `processAllTokens(tokens: Token[]): Token[]` | Process all tokens before walk tokens. |
264+
| `provideLexer(): (src: string, options?: MarkedOptions) => Token[]` | Provide function to tokenize markdown. |
265+
| `provideParser(): (tokens: Token[], options?: MarkedOptions) => string` | Provide function to parse tokens. |
264266

265267
`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*.
266268

@@ -325,6 +327,45 @@ console.log(marked.parse(`
325327
<img src="x">
326328
```
327329

330+
**Example:** Save reflinks for chunked rendering
331+
332+
```js
333+
import { marked, Lexer } from 'marked';
334+
335+
let refLinks = {};
336+
337+
// Override function
338+
function processAllTokens(tokens) {
339+
refLinks = tokens.links;
340+
return tokens;
341+
}
342+
343+
function provideLexer(src, options) {
344+
return (src, options) => {
345+
const lexer = new Lexer(options);
346+
lexer.tokens.links = refLinks;
347+
return this.block ? lexer.lex(src) : lexer.inlineTokens(src);
348+
};
349+
}
350+
351+
marked.use({ hooks: { processAllTokens, provideLexer } });
352+
353+
// Parse reflinks separately from markdown that uses them
354+
marked.parse(`
355+
[test]: http://example.com
356+
`);
357+
358+
console.log(marked.parse(`
359+
[test link][test]
360+
`));
361+
```
362+
363+
**Output:**
364+
365+
```html
366+
<p><a href="http://example.com">test link</a></p>
367+
```
368+
328369
***
329370

330371
<h2 id="extensions">Custom Extensions : <code>extensions</code></h2>

‎src/Hooks.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { _defaults } from './defaults.ts';
2+
import { _Lexer } from './Lexer.ts';
3+
import { _Parser } from './Parser.ts';
24
import type { MarkedOptions } from './MarkedOptions.ts';
35
import type { Token, TokensList } from './Tokens.ts';
46

57
export class _Hooks {
68
options: MarkedOptions;
9+
block: boolean | undefined;
710

811
constructor(options?: MarkedOptions) {
912
this.options = options || _defaults;
@@ -35,4 +38,18 @@ export class _Hooks {
3538
processAllTokens(tokens: Token[] | TokensList) {
3639
return tokens;
3740
}
41+
42+
/**
43+
* Provide function to tokenize markdown
44+
*/
45+
provideLexer() {
46+
return this.block ? _Lexer.lex : _Lexer.lexInline;
47+
}
48+
49+
/**
50+
* Provide function to parse tokens
51+
*/
52+
provideParser() {
53+
return this.block ? _Parser.parse : _Parser.parseInline;
54+
}
3855
}

‎src/Instance.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export class Marked {
1818
defaults = _getDefaults();
1919
options = this.setOptions;
2020

21-
parse = this.parseMarkdown(_Lexer.lex, _Parser.parse);
22-
parseInline = this.parseMarkdown(_Lexer.lexInline, _Parser.parseInline);
21+
parse = this.parseMarkdown(true);
22+
parseInline = this.parseMarkdown(false);
2323

2424
Parser = _Parser;
2525
Renderer = _Renderer;
@@ -195,11 +195,11 @@ export class Marked {
195195
if (!(prop in hooks)) {
196196
throw new Error(`hook '${prop}' does not exist`);
197197
}
198-
if (prop === 'options') {
199-
// ignore options property
198+
if (['options', 'block'].includes(prop)) {
199+
// ignore options and block properties
200200
continue;
201201
}
202-
const hooksProp = prop as Exclude<keyof _Hooks, 'options'>;
202+
const hooksProp = prop as Exclude<keyof _Hooks, 'options' | 'block'>;
203203
const hooksFunc = pack.hooks[hooksProp] as UnknownFunction;
204204
const prevHook = hooks[hooksProp] as UnknownFunction;
205205
if (_Hooks.passThroughHooks.has(prop)) {
@@ -261,7 +261,7 @@ export class Marked {
261261
return _Parser.parse(tokens, options ?? this.defaults);
262262
}
263263

264-
private parseMarkdown(lexer: (src: string, options?: MarkedOptions) => TokensList | Token[], parser: (tokens: Token[], options?: MarkedOptions) => string) {
264+
private parseMarkdown(blockType: boolean) {
265265
type overloadedParse = {
266266
(src: string, options: MarkedOptions & { async: true }): Promise<string>;
267267
(src: string, options: MarkedOptions & { async: false }): string;
@@ -291,8 +291,12 @@ export class Marked {
291291

292292
if (opt.hooks) {
293293
opt.hooks.options = opt;
294+
opt.hooks.block = blockType;
294295
}
295296

297+
const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);
298+
const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);
299+
296300
if (opt.async) {
297301
return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src)
298302
.then(src => lexer(src, opt))
@@ -309,7 +313,7 @@ export class Marked {
309313
}
310314
let tokens = lexer(src, opt);
311315
if (opt.hooks) {
312-
tokens = opt.hooks.processAllTokens(tokens) as Token[] | TokensList;
316+
tokens = opt.hooks.processAllTokens(tokens);
313317
}
314318
if (opt.walkTokens) {
315319
this.walkTokens(tokens, opt.walkTokens);

‎src/MarkedOptions.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface RendererExtension {
3434

3535
export type TokenizerAndRendererExtension = TokenizerExtension | RendererExtension | (TokenizerExtension & RendererExtension);
3636

37-
type HooksApi = Omit<_Hooks, 'constructor' | 'options'>;
37+
type HooksApi = Omit<_Hooks, 'constructor' | 'options' | 'block'>;
3838
type HooksObject = {
3939
[K in keyof HooksApi]?: (this: _Hooks, ...args: Parameters<HooksApi[K]>) => ReturnType<HooksApi[K]> | Promise<ReturnType<HooksApi[K]>>
4040
};
@@ -77,6 +77,8 @@ export interface MarkedExtension {
7777
* preprocess is called to process markdown before sending it to marked.
7878
* processAllTokens is called with the TokensList before walkTokens.
7979
* postprocess is called to process html after marked has finished parsing.
80+
* provideLexer is called to provide a function to tokenize markdown.
81+
* provideParser is called to provide a function to parse tokens.
8082
*/
8183
hooks?: HooksObject | undefined | null;
8284

‎test/types/marked.ts

+10
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,16 @@ marked.use({
346346
}
347347
}
348348
});
349+
marked.use({
350+
hooks: {
351+
provideLexer() {
352+
return this.block ? Lexer.lex : Lexer.lexInline;
353+
},
354+
provideParser() {
355+
return this.block ? Parser.parse : Parser.parseInline;
356+
},
357+
}
358+
});
349359
marked.use({
350360
async: true,
351361
hooks: {

‎test/unit/Hooks.test.js

+68
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,72 @@ describe('Hooks', () => {
190190
<h1>postprocess2 async</h1>
191191
<h1>postprocess1</h1>`);
192192
});
193+
194+
it('should provide lexer', () => {
195+
marked.use({
196+
hooks: {
197+
provideLexer() {
198+
return (src) => [createHeadingToken(src)];
199+
},
200+
},
201+
});
202+
const html = marked.parse('text');
203+
assert.strictEqual(html.trim(), '<h1>text</h1>');
204+
});
205+
206+
it('should provide lexer async', async() => {
207+
marked.use({
208+
async: true,
209+
hooks: {
210+
provideLexer() {
211+
return async(src) => {
212+
await timeout();
213+
return [createHeadingToken(src)];
214+
};
215+
},
216+
},
217+
});
218+
const html = await marked.parse('text');
219+
assert.strictEqual(html.trim(), '<h1>text</h1>');
220+
});
221+
222+
it('should provide parser return object', () => {
223+
marked.use({
224+
hooks: {
225+
provideParser() {
226+
return (tokens) => ({ text: 'test parser' });
227+
},
228+
},
229+
});
230+
const html = marked.parse('text');
231+
assert.strictEqual(html.text, 'test parser');
232+
});
233+
234+
it('should provide parser', () => {
235+
marked.use({
236+
hooks: {
237+
provideParser() {
238+
return (tokens) => 'test parser';
239+
},
240+
},
241+
});
242+
const html = marked.parse('text');
243+
assert.strictEqual(html.trim(), 'test parser');
244+
});
245+
246+
it('should provide parser async', async() => {
247+
marked.use({
248+
async: true,
249+
hooks: {
250+
provideParser() {
251+
return async(tokens) => {
252+
await timeout();
253+
return 'test parser';
254+
};
255+
},
256+
},
257+
});
258+
const html = await marked.parse('text');
259+
assert.strictEqual(html.trim(), 'test parser');
260+
});
193261
});

0 commit comments

Comments
 (0)
Please sign in to comment.