Skip to content

Commit faae243

Browse files
authored
feat: add processAllTokens hook (#3114)
1 parent f6450bc commit faae243

File tree

5 files changed

+126
-14
lines changed

5 files changed

+126
-14
lines changed

src/Hooks.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { _defaults } from './defaults.ts';
22
import type { MarkedOptions } from './MarkedOptions.ts';
3+
import type { Token, TokensList } from './Tokens.ts';
34

45
export class _Hooks {
56
options: MarkedOptions;
@@ -10,7 +11,8 @@ export class _Hooks {
1011

1112
static passThroughHooks = new Set([
1213
'preprocess',
13-
'postprocess'
14+
'postprocess',
15+
'processAllTokens'
1416
]);
1517

1618
/**
@@ -26,4 +28,11 @@ export class _Hooks {
2628
postprocess(html: string) {
2729
return html;
2830
}
31+
32+
/**
33+
* Process all tokens before walk tokens
34+
*/
35+
processAllTokens(tokens: Token[] | TokensList) {
36+
return tokens;
37+
}
2938
}

src/Instance.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,25 @@ export class Marked {
204204
const hooksFunc = pack.hooks[hooksProp] as UnknownFunction;
205205
const prevHook = hooks[hooksProp] as UnknownFunction;
206206
if (_Hooks.passThroughHooks.has(prop)) {
207-
hooks[hooksProp] = (arg: string | undefined) => {
207+
// @ts-expect-error cannot type hook function dynamically
208+
hooks[hooksProp] = (arg: unknown) => {
208209
if (this.defaults.async) {
209210
return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => {
210-
return prevHook.call(hooks, ret) as string;
211+
return prevHook.call(hooks, ret);
211212
});
212213
}
213214

214215
const ret = hooksFunc.call(hooks, arg);
215-
return prevHook.call(hooks, ret) as string;
216+
return prevHook.call(hooks, ret);
216217
};
217218
} else {
219+
// @ts-expect-error cannot type hook function dynamically
218220
hooks[hooksProp] = (...args: unknown[]) => {
219221
let ret = hooksFunc.apply(hooks, args);
220222
if (ret === false) {
221223
ret = prevHook.apply(hooks, args);
222224
}
223-
return ret as string;
225+
return ret;
224226
};
225227
}
226228
}
@@ -292,6 +294,7 @@ export class Marked {
292294
if (opt.async) {
293295
return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src)
294296
.then(src => lexer(src, opt))
297+
.then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens)
295298
.then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens)
296299
.then(tokens => parser(tokens, opt))
297300
.then(html => opt.hooks ? opt.hooks.postprocess(html) : html)
@@ -302,7 +305,10 @@ export class Marked {
302305
if (opt.hooks) {
303306
src = opt.hooks.preprocess(src) as string;
304307
}
305-
const tokens = lexer(src, opt);
308+
let tokens = lexer(src, opt);
309+
if (opt.hooks) {
310+
tokens = opt.hooks.processAllTokens(tokens) as Token[] | TokensList;
311+
}
306312
if (opt.walkTokens) {
307313
this.walkTokens(tokens, opt.walkTokens);
308314
}

src/MarkedOptions.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { _Parser } from './Parser.ts';
33
import type { _Lexer } from './Lexer.ts';
44
import type { _Renderer } from './Renderer.ts';
55
import type { _Tokenizer } from './Tokenizer.ts';
6+
import type { _Hooks } from './Hooks.ts';
67

78
export interface TokenizerThis {
89
lexer: _Lexer;
@@ -33,6 +34,11 @@ export interface RendererExtension {
3334

3435
export type TokenizerAndRendererExtension = TokenizerExtension | RendererExtension | (TokenizerExtension & RendererExtension);
3536

37+
type HooksApi = Omit<_Hooks, 'constructor' | 'options'>;
38+
type HooksObject = {
39+
[K in keyof HooksApi]?: (...args: Parameters<HooksApi[K]>) => ReturnType<HooksApi[K]> | Promise<ReturnType<HooksApi[K]>>
40+
};
41+
3642
type RendererApi = Omit<_Renderer, 'constructor' | 'options'>;
3743
type RendererObject = {
3844
[K in keyof RendererApi]?: (...args: Parameters<RendererApi[K]>) => ReturnType<RendererApi[K]> | false
@@ -69,14 +75,10 @@ export interface MarkedExtension {
6975
/**
7076
* Hooks are methods that hook into some part of marked.
7177
* preprocess is called to process markdown before sending it to marked.
78+
* processAllTokens is called with the TokensList before walkTokens.
7279
* postprocess is called to process html after marked has finished parsing.
7380
*/
74-
hooks?: {
75-
preprocess: (markdown: string) => string | Promise<string>,
76-
postprocess: (html: string) => string | Promise<string>,
77-
// eslint-disable-next-line no-use-before-define
78-
options?: MarkedOptions
79-
} | null;
81+
hooks?: HooksObject | undefined | null;
8082

8183
/**
8284
* Conform to obscure parts of markdown.pl as much as possible. Don't fix any of the original markdown bugs or poor behavior.
@@ -109,7 +111,12 @@ export interface MarkedExtension {
109111
walkTokens?: ((token: Token) => void | Promise<void>) | undefined | null;
110112
}
111113

112-
export interface MarkedOptions extends Omit<MarkedExtension, 'renderer' | 'tokenizer' | 'extensions' | 'walkTokens'> {
114+
export interface MarkedOptions extends Omit<MarkedExtension, 'hooks' | 'renderer' | 'tokenizer' | 'extensions' | 'walkTokens'> {
115+
/**
116+
* Hooks are methods that hook into some part of marked.
117+
*/
118+
hooks?: _Hooks | undefined | null;
119+
113120
/**
114121
* Type: object Default: new Renderer()
115122
*

test/types/marked.ts

+21
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,24 @@ marked.use({
323323
}
324324
}
325325
});
326+
marked.use({
327+
hooks: {
328+
processAllTokens(tokens) {
329+
return tokens;
330+
}
331+
}
332+
});
333+
marked.use({
334+
async: true,
335+
hooks: {
336+
async preprocess(markdown) {
337+
return markdown;
338+
},
339+
async postprocess(html) {
340+
return html;
341+
},
342+
async processAllTokens(tokens) {
343+
return tokens;
344+
}
345+
}
346+
});

test/unit/Hooks.test.js

+70-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { timeout } from './utils.js';
33
import { describe, it, beforeEach } from 'node:test';
44
import assert from 'node:assert';
55

6+
function createHeadingToken(text) {
7+
return {
8+
type: 'heading',
9+
raw: `# ${text}`,
10+
depth: 1,
11+
text,
12+
tokens: [
13+
{ type: 'text', raw: text, text }
14+
]
15+
};
16+
}
17+
618
describe('Hooks', () => {
719
let marked;
820
beforeEach(() => {
@@ -93,6 +105,48 @@ describe('Hooks', () => {
93105
assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>postprocess async</h1>');
94106
});
95107

108+
it('should process tokens before walkTokens', () => {
109+
marked.use({
110+
hooks: {
111+
processAllTokens(tokens) {
112+
tokens.push(createHeadingToken('processAllTokens'));
113+
return tokens;
114+
}
115+
},
116+
walkTokens(token) {
117+
if (token.type === 'heading') {
118+
token.tokens[0].text += ' walked';
119+
}
120+
return token;
121+
}
122+
});
123+
const html = marked.parse('*text*');
124+
assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>processAllTokens walked</h1>');
125+
});
126+
127+
it('should process tokens async before walkTokens', async() => {
128+
marked.use({
129+
async: true,
130+
hooks: {
131+
async processAllTokens(tokens) {
132+
await timeout();
133+
tokens.push(createHeadingToken('processAllTokens async'));
134+
return tokens;
135+
}
136+
},
137+
walkTokens(token) {
138+
if (token.type === 'heading') {
139+
token.tokens[0].text += ' walked';
140+
}
141+
return token;
142+
}
143+
});
144+
const promise = marked.parse('*text*');
145+
assert.ok(promise instanceof Promise);
146+
const html = await promise;
147+
assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>processAllTokens async walked</h1>');
148+
});
149+
96150
it('should process all hooks in reverse', async() => {
97151
marked.use({
98152
hooks: {
@@ -101,6 +155,10 @@ describe('Hooks', () => {
101155
},
102156
postprocess(html) {
103157
return html + '<h1>postprocess1</h1>\n';
158+
},
159+
processAllTokens(tokens) {
160+
tokens.push(createHeadingToken('processAllTokens1'));
161+
return tokens;
104162
}
105163
}
106164
});
@@ -113,12 +171,23 @@ describe('Hooks', () => {
113171
async postprocess(html) {
114172
await timeout();
115173
return html + '<h1>postprocess2 async</h1>\n';
174+
},
175+
processAllTokens(tokens) {
176+
tokens.push(createHeadingToken('processAllTokens2'));
177+
return tokens;
116178
}
117179
}
118180
});
119181
const promise = marked.parse('*text*');
120182
assert.ok(promise instanceof Promise);
121183
const html = await promise;
122-
assert.strictEqual(html.trim(), '<h1>preprocess1</h1>\n<h1>preprocess2</h1>\n<p><em>text</em></p>\n<h1>postprocess2 async</h1>\n<h1>postprocess1</h1>');
184+
assert.strictEqual(html.trim(), `\
185+
<h1>preprocess1</h1>
186+
<h1>preprocess2</h1>
187+
<p><em>text</em></p>
188+
<h1>processAllTokens2</h1>
189+
<h1>processAllTokens1</h1>
190+
<h1>postprocess2 async</h1>
191+
<h1>postprocess1</h1>`);
123192
});
124193
});

0 commit comments

Comments
 (0)