Skip to content

Commit 6e96fa7

Browse files
authored
fix: avoid race condition in async parallel parse/parseInline with hooks (#3924)
1 parent 73e1f3f commit 6e96fa7

3 files changed

Lines changed: 202 additions & 8 deletions

File tree

src/Hooks.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ export class _Hooks<ParserOutput = string, RendererOutput = string> {
5656
/**
5757
* Provide function to tokenize markdown
5858
*/
59-
provideLexer() {
60-
return this.block ? _Lexer.lex : _Lexer.lexInline;
59+
provideLexer(block = this.block) {
60+
return block ? _Lexer.lex : _Lexer.lexInline;
6161
}
6262

6363
/**
6464
* Provide function to parse tokens
6565
*/
66-
provideParser() {
67-
return this.block ? _Parser.parse<ParserOutput, RendererOutput> : _Parser.parseInline<ParserOutput, RendererOutput>;
66+
provideParser(block = this.block) {
67+
return block ? _Parser.parse<ParserOutput, RendererOutput> : _Parser.parseInline<ParserOutput, RendererOutput>;
6868
}
6969
}

src/Instance.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,13 @@ export class Marked<ParserOutput = string, RendererOutput = string> {
308308
if (opt.async) {
309309
return (async() => {
310310
const processedSrc = opt.hooks ? await opt.hooks.preprocess(src) : src;
311-
const lexer = opt.hooks ? await opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);
311+
const lexer = opt.hooks ? await opt.hooks.provideLexer(blockType) : (blockType ? _Lexer.lex : _Lexer.lexInline);
312312
const tokens = await lexer(processedSrc, opt);
313313
const processedTokens = opt.hooks ? await opt.hooks.processAllTokens(tokens) : tokens;
314314
if (opt.walkTokens) {
315315
await Promise.all(this.walkTokens(processedTokens, opt.walkTokens));
316316
}
317-
const parser = opt.hooks ? await opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);
317+
const parser = opt.hooks ? await opt.hooks.provideParser(blockType) : (blockType ? _Parser.parse : _Parser.parseInline);
318318
const html = await parser(processedTokens, opt);
319319
return opt.hooks ? await opt.hooks.postprocess(html) : html;
320320
})().catch(throwError);
@@ -324,15 +324,15 @@ export class Marked<ParserOutput = string, RendererOutput = string> {
324324
if (opt.hooks) {
325325
src = opt.hooks.preprocess(src) as string;
326326
}
327-
const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);
327+
const lexer = opt.hooks ? opt.hooks.provideLexer(blockType) : (blockType ? _Lexer.lex : _Lexer.lexInline);
328328
let tokens = lexer(src, opt);
329329
if (opt.hooks) {
330330
tokens = opt.hooks.processAllTokens(tokens);
331331
}
332332
if (opt.walkTokens) {
333333
this.walkTokens(tokens, opt.walkTokens);
334334
}
335-
const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);
335+
const parser = opt.hooks ? opt.hooks.provideParser(blockType) : (blockType ? _Parser.parse : _Parser.parseInline);
336336
let html = parser(tokens, opt);
337337
if (opt.hooks) {
338338
html = opt.hooks.postprocess(html);

test/unit/Hooks.test.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,198 @@ describe('Hooks', () => {
324324
const html = await marked.parse('text');
325325
assert.strictEqual(html.trim(), 'test parser');
326326
});
327+
328+
it('should not have race condition when parse and parseInline are called concurrently with async hooks', async() => {
329+
marked.use({
330+
async: true,
331+
hooks: {
332+
async preprocess(markdown) {
333+
await timeout();
334+
return markdown;
335+
},
336+
},
337+
});
338+
const [blockHtml, inlineHtml] = await Promise.all([
339+
marked.parse('**text**'),
340+
marked.parseInline('**text**'),
341+
]);
342+
assert.strictEqual(blockHtml.trim(), '<p><strong>text</strong></p>');
343+
assert.strictEqual(inlineHtml.trim(), '<strong>text</strong>');
344+
});
345+
346+
it('should not have race condition with multiple concurrent parse calls', async() => {
347+
marked.use({
348+
async: true,
349+
hooks: {
350+
async preprocess(markdown) {
351+
await timeout();
352+
return markdown;
353+
},
354+
},
355+
});
356+
const [html1, html2, html3] = await Promise.all([
357+
marked.parse('**bold**'),
358+
marked.parseInline('**bold**'),
359+
marked.parse('*italic*'),
360+
]);
361+
assert.strictEqual(html1.trim(), '<p><strong>bold</strong></p>');
362+
assert.strictEqual(html2.trim(), '<strong>bold</strong>');
363+
assert.strictEqual(html3.trim(), '<p><em>italic</em></p>');
364+
});
365+
366+
it('should pass block=true to provideLexer when called from parse', () => {
367+
let receivedBlock;
368+
marked.use({
369+
hooks: {
370+
provideLexer(block) {
371+
receivedBlock = block;
372+
return () => [];
373+
},
374+
},
375+
});
376+
marked.parse('text');
377+
assert.strictEqual(receivedBlock, true);
378+
});
379+
380+
it('should pass block=false to provideLexer when called from parseInline', () => {
381+
let receivedBlock;
382+
marked.use({
383+
hooks: {
384+
provideLexer(block) {
385+
receivedBlock = block;
386+
return () => [];
387+
},
388+
},
389+
});
390+
marked.parseInline('text');
391+
assert.strictEqual(receivedBlock, false);
392+
});
393+
394+
it('should pass correct block to provideLexer for concurrent async parse and parseInline', async() => {
395+
const receivedBlocks = [];
396+
marked.use({
397+
async: true,
398+
hooks: {
399+
async preprocess(markdown) {
400+
await timeout();
401+
return markdown;
402+
},
403+
provideLexer(block) {
404+
receivedBlocks.push(block);
405+
return () => [];
406+
},
407+
},
408+
});
409+
await Promise.all([
410+
marked.parse('text'),
411+
marked.parseInline('text'),
412+
]);
413+
assert.deepStrictEqual(receivedBlocks.slice().sort(), [false, true]);
414+
});
415+
416+
it('should pass block=true to provideParser when called from parse', () => {
417+
let receivedBlock;
418+
marked.use({
419+
hooks: {
420+
provideParser(block) {
421+
receivedBlock = block;
422+
return () => '';
423+
},
424+
},
425+
});
426+
marked.parse('text');
427+
assert.strictEqual(receivedBlock, true);
428+
});
429+
430+
it('should pass block=false to provideParser when called from parseInline', () => {
431+
let receivedBlock;
432+
marked.use({
433+
hooks: {
434+
provideParser(block) {
435+
receivedBlock = block;
436+
return () => '';
437+
},
438+
},
439+
});
440+
marked.parseInline('text');
441+
assert.strictEqual(receivedBlock, false);
442+
});
443+
444+
it('should pass correct block to provideParser for concurrent async parse and parseInline', async() => {
445+
const receivedBlocks = [];
446+
marked.use({
447+
async: true,
448+
hooks: {
449+
async preprocess(markdown) {
450+
await timeout();
451+
return markdown;
452+
},
453+
provideParser(block) {
454+
receivedBlocks.push(block);
455+
return () => '';
456+
},
457+
},
458+
});
459+
await Promise.all([
460+
marked.parse('text'),
461+
marked.parseInline('text'),
462+
]);
463+
assert.deepStrictEqual(receivedBlocks.slice().sort(), [false, true]);
464+
});
465+
466+
it('should maintain this.block backwards compatibility in provideLexer for parse', () => {
467+
let blockFromThis;
468+
marked.use({
469+
hooks: {
470+
provideLexer() {
471+
blockFromThis = this.block;
472+
return () => [];
473+
},
474+
},
475+
});
476+
marked.parse('text');
477+
assert.strictEqual(blockFromThis, true);
478+
});
479+
480+
it('should maintain this.block backwards compatibility in provideLexer for parseInline', () => {
481+
let blockFromThis;
482+
marked.use({
483+
hooks: {
484+
provideLexer() {
485+
blockFromThis = this.block;
486+
return () => [];
487+
},
488+
},
489+
});
490+
marked.parseInline('text');
491+
assert.strictEqual(blockFromThis, false);
492+
});
493+
494+
it('should maintain this.block backwards compatibility in provideParser for parse', () => {
495+
let blockFromThis;
496+
marked.use({
497+
hooks: {
498+
provideParser() {
499+
blockFromThis = this.block;
500+
return () => '';
501+
},
502+
},
503+
});
504+
marked.parse('text');
505+
assert.strictEqual(blockFromThis, true);
506+
});
507+
508+
it('should maintain this.block backwards compatibility in provideParser for parseInline', () => {
509+
let blockFromThis;
510+
marked.use({
511+
hooks: {
512+
provideParser() {
513+
blockFromThis = this.block;
514+
return () => '';
515+
},
516+
},
517+
});
518+
marked.parseInline('text');
519+
assert.strictEqual(blockFromThis, false);
520+
});
327521
});

0 commit comments

Comments
 (0)