Testing Hook System Implementation in Marked Parser
This test suite examines the hook functionality in the Marked markdown parser, focusing on preprocessing, postprocessing, and token manipulation capabilities. The tests verify both synchronous and asynchronous hook operations, ensuring robust markdown transformation and HTML generation.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
markedjs/marked
test/unit/Hooks.test.js
import { Marked } from '../../lib/marked.esm.js';
import { timeout } from './utils.js';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
function createHeadingToken(text) {
return {
type: 'heading',
raw: `# ${text}`,
depth: 1,
text,
tokens: [
{ type: 'text', raw: text, text },
],
};
}
describe('Hooks', () => {
let marked;
beforeEach(() => {
marked = new Marked();
});
it('should preprocess markdown', () => {
marked.use({
hooks: {
preprocess(markdown) {
return `# preprocess\n\n${markdown}`;
},
},
});
const html = marked.parse('*text*');
assert.strictEqual(html.trim(), '<h1>preprocess</h1>\n<p><em>text</em></p>');
});
it('should preprocess async', async() => {
marked.use({
async: true,
hooks: {
async preprocess(markdown) {
await timeout();
return `# preprocess async\n\n${markdown}`;
},
},
});
const promise = marked.parse('*text*');
assert.ok(promise instanceof Promise);
const html = await promise;
assert.strictEqual(html.trim(), '<h1>preprocess async</h1>\n<p><em>text</em></p>');
});
it('should preprocess options', () => {
marked.use({
hooks: {
preprocess(markdown) {
this.options.breaks = true;
return markdown;
},
},
});
const html = marked.parse('line1\nline2');
assert.strictEqual(html.trim(), '<p>line1<br>line2</p>');
});
it('should preprocess options async', async() => {
marked.use({
async: true,
hooks: {
async preprocess(markdown) {
await timeout();
this.options.breaks = true;
return markdown;
},
},
});
const html = await marked.parse('line1\nline2');
assert.strictEqual(html.trim(), '<p>line1<br>line2</p>');
});
it('should postprocess html', () => {
marked.use({
hooks: {
postprocess(html) {
return html + '<h1>postprocess</h1>';
},
},
});
const html = marked.parse('*text*');
assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>postprocess</h1>');
});
it('should postprocess async', async() => {
marked.use({
async: true,
hooks: {
async postprocess(html) {
await timeout();
return html + '<h1>postprocess async</h1>\n';
},
},
});
const promise = marked.parse('*text*');
assert.ok(promise instanceof Promise);
const html = await promise;
assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>postprocess async</h1>');
});
it('should process tokens before walkTokens', () => {
marked.use({
hooks: {
processAllTokens(tokens) {
tokens.push(createHeadingToken('processAllTokens'));
return tokens;
},
},
walkTokens(token) {
if (token.type === 'heading') {
token.tokens[0].text += ' walked';
}
return token;
},
});
const html = marked.parse('*text*');
assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>processAllTokens walked</h1>');
});
it('should process tokens async before walkTokens', async() => {
marked.use({
async: true,
hooks: {
async processAllTokens(tokens) {
await timeout();
tokens.push(createHeadingToken('processAllTokens async'));
return tokens;
},
},
walkTokens(token) {
if (token.type === 'heading') {
token.tokens[0].text += ' walked';
}
return token;
},
});
const promise = marked.parse('*text*');
assert.ok(promise instanceof Promise);
const html = await promise;
assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>processAllTokens async walked</h1>');
});
it('should process all hooks in reverse', async() => {
marked.use({
hooks: {
preprocess(markdown) {
return `# preprocess1\n\n${markdown}`;
},
postprocess(html) {
return html + '<h1>postprocess1</h1>\n';
},
processAllTokens(tokens) {
tokens.push(createHeadingToken('processAllTokens1'));
return tokens;
},
},
});
marked.use({
async: true,
hooks: {
preprocess(markdown) {
return `# preprocess2\n\n${markdown}`;
},
async postprocess(html) {
await timeout();
return html + '<h1>postprocess2 async</h1>\n';
},
processAllTokens(tokens) {
tokens.push(createHeadingToken('processAllTokens2'));
return tokens;
},
},
});
const promise = marked.parse('*text*');
assert.ok(promise instanceof Promise);
const html = await promise;
assert.strictEqual(html.trim(), `\
<h1>preprocess1</h1>
<h1>preprocess2</h1>
<p><em>text</em></p>
<h1>processAllTokens2</h1>
<h1>processAllTokens1</h1>
<h1>postprocess2 async</h1>
<h1>postprocess1</h1>`);
});
it('should provide lexer', () => {
marked.use({
hooks: {
provideLexer() {
return (src) => [createHeadingToken(src)];
},
},
});
const html = marked.parse('text');
assert.strictEqual(html.trim(), '<h1>text</h1>');
});
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(), '<h1>text</h1>');
});
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');
});
});