diff --git a/src/Hooks.ts b/src/Hooks.ts index 6dca1bd781..febeb581a2 100644 --- a/src/Hooks.ts +++ b/src/Hooks.ts @@ -56,14 +56,14 @@ export class _Hooks { /** * Provide function to tokenize markdown */ - provideLexer() { - return this.block ? _Lexer.lex : _Lexer.lexInline; + provideLexer(block = this.block) { + return block ? _Lexer.lex : _Lexer.lexInline; } /** * Provide function to parse tokens */ - provideParser() { - return this.block ? _Parser.parse : _Parser.parseInline; + provideParser(block = this.block) { + return block ? _Parser.parse : _Parser.parseInline; } } diff --git a/src/Instance.ts b/src/Instance.ts index 396ad33943..d21c0117a8 100644 --- a/src/Instance.ts +++ b/src/Instance.ts @@ -308,13 +308,13 @@ export class Marked { if (opt.async) { return (async() => { const processedSrc = opt.hooks ? await opt.hooks.preprocess(src) : src; - const lexer = opt.hooks ? await opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); + const lexer = opt.hooks ? await opt.hooks.provideLexer(blockType) : (blockType ? _Lexer.lex : _Lexer.lexInline); const tokens = await lexer(processedSrc, opt); const processedTokens = opt.hooks ? await opt.hooks.processAllTokens(tokens) : tokens; if (opt.walkTokens) { await Promise.all(this.walkTokens(processedTokens, opt.walkTokens)); } - const parser = opt.hooks ? await opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); + const parser = opt.hooks ? await opt.hooks.provideParser(blockType) : (blockType ? _Parser.parse : _Parser.parseInline); const html = await parser(processedTokens, opt); return opt.hooks ? await opt.hooks.postprocess(html) : html; })().catch(throwError); @@ -324,7 +324,7 @@ export class Marked { if (opt.hooks) { src = opt.hooks.preprocess(src) as string; } - const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); + const lexer = opt.hooks ? opt.hooks.provideLexer(blockType) : (blockType ? _Lexer.lex : _Lexer.lexInline); let tokens = lexer(src, opt); if (opt.hooks) { tokens = opt.hooks.processAllTokens(tokens); @@ -332,7 +332,7 @@ export class Marked { if (opt.walkTokens) { this.walkTokens(tokens, opt.walkTokens); } - const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); + const parser = opt.hooks ? opt.hooks.provideParser(blockType) : (blockType ? _Parser.parse : _Parser.parseInline); let html = parser(tokens, opt); if (opt.hooks) { html = opt.hooks.postprocess(html); diff --git a/test/unit/Hooks.test.js b/test/unit/Hooks.test.js index a4ce622e33..c9a1c9a48f 100644 --- a/test/unit/Hooks.test.js +++ b/test/unit/Hooks.test.js @@ -324,4 +324,198 @@ describe('Hooks', () => { const html = await marked.parse('text'); assert.strictEqual(html.trim(), 'test parser'); }); + + it('should not have race condition when parse and parseInline are called concurrently with async hooks', async() => { + marked.use({ + async: true, + hooks: { + async preprocess(markdown) { + await timeout(); + return markdown; + }, + }, + }); + const [blockHtml, inlineHtml] = await Promise.all([ + marked.parse('**text**'), + marked.parseInline('**text**'), + ]); + assert.strictEqual(blockHtml.trim(), '

text

'); + assert.strictEqual(inlineHtml.trim(), 'text'); + }); + + it('should not have race condition with multiple concurrent parse calls', async() => { + marked.use({ + async: true, + hooks: { + async preprocess(markdown) { + await timeout(); + return markdown; + }, + }, + }); + const [html1, html2, html3] = await Promise.all([ + marked.parse('**bold**'), + marked.parseInline('**bold**'), + marked.parse('*italic*'), + ]); + assert.strictEqual(html1.trim(), '

bold

'); + assert.strictEqual(html2.trim(), 'bold'); + assert.strictEqual(html3.trim(), '

italic

'); + }); + + it('should pass block=true to provideLexer when called from parse', () => { + let receivedBlock; + marked.use({ + hooks: { + provideLexer(block) { + receivedBlock = block; + return () => []; + }, + }, + }); + marked.parse('text'); + assert.strictEqual(receivedBlock, true); + }); + + it('should pass block=false to provideLexer when called from parseInline', () => { + let receivedBlock; + marked.use({ + hooks: { + provideLexer(block) { + receivedBlock = block; + return () => []; + }, + }, + }); + marked.parseInline('text'); + assert.strictEqual(receivedBlock, false); + }); + + it('should pass correct block to provideLexer for concurrent async parse and parseInline', async() => { + const receivedBlocks = []; + marked.use({ + async: true, + hooks: { + async preprocess(markdown) { + await timeout(); + return markdown; + }, + provideLexer(block) { + receivedBlocks.push(block); + return () => []; + }, + }, + }); + await Promise.all([ + marked.parse('text'), + marked.parseInline('text'), + ]); + assert.deepStrictEqual(receivedBlocks.slice().sort(), [false, true]); + }); + + it('should pass block=true to provideParser when called from parse', () => { + let receivedBlock; + marked.use({ + hooks: { + provideParser(block) { + receivedBlock = block; + return () => ''; + }, + }, + }); + marked.parse('text'); + assert.strictEqual(receivedBlock, true); + }); + + it('should pass block=false to provideParser when called from parseInline', () => { + let receivedBlock; + marked.use({ + hooks: { + provideParser(block) { + receivedBlock = block; + return () => ''; + }, + }, + }); + marked.parseInline('text'); + assert.strictEqual(receivedBlock, false); + }); + + it('should pass correct block to provideParser for concurrent async parse and parseInline', async() => { + const receivedBlocks = []; + marked.use({ + async: true, + hooks: { + async preprocess(markdown) { + await timeout(); + return markdown; + }, + provideParser(block) { + receivedBlocks.push(block); + return () => ''; + }, + }, + }); + await Promise.all([ + marked.parse('text'), + marked.parseInline('text'), + ]); + assert.deepStrictEqual(receivedBlocks.slice().sort(), [false, true]); + }); + + it('should maintain this.block backwards compatibility in provideLexer for parse', () => { + let blockFromThis; + marked.use({ + hooks: { + provideLexer() { + blockFromThis = this.block; + return () => []; + }, + }, + }); + marked.parse('text'); + assert.strictEqual(blockFromThis, true); + }); + + it('should maintain this.block backwards compatibility in provideLexer for parseInline', () => { + let blockFromThis; + marked.use({ + hooks: { + provideLexer() { + blockFromThis = this.block; + return () => []; + }, + }, + }); + marked.parseInline('text'); + assert.strictEqual(blockFromThis, false); + }); + + it('should maintain this.block backwards compatibility in provideParser for parse', () => { + let blockFromThis; + marked.use({ + hooks: { + provideParser() { + blockFromThis = this.block; + return () => ''; + }, + }, + }); + marked.parse('text'); + assert.strictEqual(blockFromThis, true); + }); + + it('should maintain this.block backwards compatibility in provideParser for parseInline', () => { + let blockFromThis; + marked.use({ + hooks: { + provideParser() { + blockFromThis = this.block; + return () => ''; + }, + }, + }); + marked.parseInline('text'); + assert.strictEqual(blockFromThis, false); + }); });