Skip to content

Commit cd2b5f4

Browse files
feat: add emStrongMask hook (#3749)
* Add hook to mask input for em/strong processing in extension * Use passthrough hook instead of tokenizer-specific hook * Remove deprecated items * Ensure passthrough hook does not create promise * Add test for combining hooks and async
1 parent e3497a5 commit cd2b5f4

6 files changed

Lines changed: 113 additions & 1 deletion

File tree

docs/USING_PRO.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ Hooks are methods that hook into some part of marked. The following hooks are av
263263
| `preprocess(markdown: string): string` | Process markdown before sending it to marked. |
264264
| `postprocess(html: string): string` | Process html after marked has finished parsing. |
265265
| `processAllTokens(tokens: Token[]): Token[]` | Process all tokens before walk tokens. |
266+
| `emStrongMask(src: string): string` | Mask part of the content that should not be interpreted as Markdown em/strong delimiters. |
266267
| `provideLexer(): (src: string, options?: MarkedOptions) => Token[]` | Provide function to tokenize markdown. |
267268
| `provideParser(): (tokens: Token[], options?: MarkedOptions) => string` | Provide function to parse tokens. |
268269

@@ -368,6 +369,27 @@ console.log(marked.parse(`
368369
<p><a href="http://example.com">test link</a></p>
369370
```
370371

372+
**Example:** Mask underline characters inside Mathjax content delimited by `$`
373+
374+
```js
375+
import { marked } from 'marked';
376+
377+
// Override function
378+
function emStrongMask(src) {
379+
return src.replace(/\$([^$]+)\$/g, (match) => `[${'a'.repeat(match.length - 2)}]`);
380+
}
381+
382+
marked.use({ hooks: { emStrongMask } });
383+
384+
console.log(marked.parse(`_The formula is $a_ b=c_ d$._`));
385+
```
386+
387+
**Output:**
388+
389+
```html
390+
<p><em>The formula is $a_ b=c_ d$.</em></p>
391+
```
392+
371393
***
372394

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

src/Hooks.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export class _Hooks<ParserOutput = string, RendererOutput = string> {
1616
'preprocess',
1717
'postprocess',
1818
'processAllTokens',
19+
'emStrongMask',
20+
]);
21+
22+
static passThroughHooksRespectAsync = new Set([
23+
'preprocess',
24+
'postprocess',
25+
'processAllTokens',
1926
]);
2027

2128
/**
@@ -39,6 +46,13 @@ export class _Hooks<ParserOutput = string, RendererOutput = string> {
3946
return tokens;
4047
}
4148

49+
/**
50+
* Mask contents that should not be interpreted as em/strong delimiters
51+
*/
52+
emStrongMask(src: string) {
53+
return src;
54+
}
55+
4256
/**
4357
* Provide function to tokenize markdown
4458
*/

src/Instance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export class Marked<ParserOutput = string, RendererOutput = string> {
205205
if (_Hooks.passThroughHooks.has(prop)) {
206206
// @ts-expect-error cannot type hook function dynamically
207207
hooks[hooksProp] = (arg: unknown) => {
208-
if (this.defaults.async) {
208+
if (this.defaults.async && _Hooks.passThroughHooksRespectAsync.has(prop)) {
209209
return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => {
210210
return prevHook.call(hooks, ret);
211211
});

src/Lexer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ export class _Lexer<ParserOutput = string, RendererOutput = string> {
324324
maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);
325325
}
326326

327+
// Mask out blocks from extensions
328+
maskedSrc = this.options.hooks?.emStrongMask?.call({ lexer: this }, maskedSrc) ?? maskedSrc;
329+
327330
let keepPrevChar = false;
328331
let prevChar = '';
329332
while (src) {

src/MarkedOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface MarkedExtension<ParserOutput = string, RendererOutput = string>
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+
* emStrongMask is called to mask contents that should not be interpreted as em/strong delimiters.
8081
* provideLexer is called to provide a function to tokenize markdown.
8182
* provideParser is called to provide a function to parse tokens.
8283
*/

test/unit/marked.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,46 @@ describe('marked unit', () => {
166166
assert.strictEqual(html, '<p>Not Underlined <u>Underlined</u> Not Underlined</p>\n');
167167
});
168168

169+
it('should ignore em termination characters when emStrongMask hook is in place', () => {
170+
const underline = {
171+
name: 'underline',
172+
level: 'inline',
173+
start(src) { return src.indexOf('='); },
174+
tokenizer(src) {
175+
const rule = /^=([^=]+)=/;
176+
const match = rule.exec(src);
177+
if (match) {
178+
return {
179+
type: 'underline',
180+
raw: match[0], // This is the text that you want your token to consume from the source
181+
text: match[1].trim(), // You can add additional properties to your tokens to pass along to the renderer
182+
};
183+
}
184+
},
185+
renderer(token) {
186+
return `<u>${token.text}</u>`;
187+
},
188+
};
189+
marked.use({
190+
hooks: {
191+
// Underline takes priority over emphasis in this example, to mask emphasis markers inside underline tags
192+
emStrongMask: (src) => src.replace(/=([^=]+)=/g, (match) => `[${'a'.repeat(match.length - 2)}]`),
193+
},
194+
extensions: [underline],
195+
});
196+
const html = marked.parse('*Not Underlined =Underlined* with *asterisk= Not Underlined*');
197+
assert.strictEqual(html, '<p><em>Not Underlined <u>Underlined* with *asterisk</u> Not Underlined</em></p>\n');
198+
});
199+
200+
it('should combine multiple emStrongMask hooks', () => {
201+
const maskEqualSign = (src) => src.replace(/=([^=]+)=/g, (match) => `[${'a'.repeat(match.length - 2)}]`);
202+
const maskDollarSign = (src) => src.replace(/\$([^$]+)\$/g, (match) => `[${'b'.repeat(match.length - 2)}]`);
203+
marked.use({ hooks: { emStrongMask: maskEqualSign } });
204+
marked.use({ hooks: { emStrongMask: maskDollarSign } });
205+
const html = marked.parse('*Before $dollar * dollar$ =equal * equal= after*');
206+
assert.strictEqual(html, '<p><em>Before $dollar * dollar$ =equal * equal= after</em></p>\n');
207+
});
208+
169209
it('should handle interacting block and inline extensions', () => {
170210
const descriptionlist = {
171211
name: 'descriptionList',
@@ -650,6 +690,38 @@ used extension2 walked</p>
650690
assert.throws(() => marked.parse('test', { async: false }));
651691
});
652692

693+
it('should ignore em termination characters when emStrongMask hook is in place in an async context', async() => {
694+
const underline = {
695+
name: 'underline',
696+
level: 'inline',
697+
start(src) { return src.indexOf('='); },
698+
tokenizer(src) {
699+
const rule = /^=([^=]+)=/;
700+
const match = rule.exec(src);
701+
if (match) {
702+
return {
703+
type: 'underline',
704+
raw: match[0], // This is the text that you want your token to consume from the source
705+
text: match[1].trim(), // You can add additional properties to your tokens to pass along to the renderer
706+
};
707+
}
708+
},
709+
renderer(token) {
710+
return `<u>${token.text}</u>`;
711+
},
712+
};
713+
marked.use({
714+
hooks: {
715+
// Underline takes priority over emphasis in this example, to mask emphasis markers inside underline tags
716+
emStrongMask: (src) => src.replace(/=([^=]+)=/g, (match) => `[${'a'.repeat(match.length - 2)}]`),
717+
},
718+
extensions: [underline],
719+
async: true,
720+
});
721+
const html = await marked.parse('*Not Underlined =Underlined* with *asterisk= Not Underlined*');
722+
assert.strictEqual(html, '<p><em>Not Underlined <u>Underlined* with *asterisk</u> Not Underlined</em></p>\n');
723+
});
724+
653725
it('should allow deleting/editing tokens', () => {
654726
const styleTags = {
655727
extensions: [{

0 commit comments

Comments
 (0)