From 2d052f55faf49877d58146a2fe4dc18a73ebfccd Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 09:45:39 -0600 Subject: [PATCH 01/13] add hooks --- src/Hooks.js | 20 +++++++++++++++ src/defaults.js | 1 + src/marked.js | 68 +++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/Hooks.js diff --git a/src/Hooks.js b/src/Hooks.js new file mode 100644 index 0000000000..5ceae59a74 --- /dev/null +++ b/src/Hooks.js @@ -0,0 +1,20 @@ +export class Hooks { + static passThroughHooks = new Set([ + 'preprocess', + 'postprocess' + ]); + + /** + * Process markdown before marked + */ + preprocess(markdown) { + return markdown; + } + + /** + * Process HTML after marked is finished + */ + postprocess(html) { + return html; + } +} diff --git a/src/defaults.js b/src/defaults.js index a1ae513688..bce0ea49d6 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -8,6 +8,7 @@ export function getDefaults() { headerIds: true, headerPrefix: '', highlight: null, + hooks: null, langPrefix: 'language-', mangle: true, pedantic: false, diff --git a/src/marked.js b/src/marked.js index 7d5e23dd26..d80bc5a011 100644 --- a/src/marked.js +++ b/src/marked.js @@ -4,6 +4,7 @@ import { Tokenizer } from './Tokenizer.js'; import { Renderer } from './Renderer.js'; import { TextRenderer } from './TextRenderer.js'; import { Slugger } from './Slugger.js'; +import { Hooks } from './Hooks.js'; import { merge, checkSanitizeDeprecation, @@ -37,10 +38,14 @@ export function marked(src, opt, callback) { checkSanitizeDeprecation(opt); if (callback) { + marked.setOptions({ async: false }); const highlight = opt.highlight; let tokens; try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } tokens = Lexer.lex(src, opt); } catch (e) { return callback(e); @@ -55,6 +60,9 @@ export function marked(src, opt, callback) { marked.walkTokens(tokens, opt.walkTokens); } out = Parser.parse(tokens, opt); + if (opt.hooks) { + out = opt.hooks.postprocess(out); + } } catch (e) { err = e; } @@ -107,6 +115,7 @@ export function marked(src, opt, callback) { function onError(e) { e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (opt.silent) { const msg = '

An error occurred:

'
         + escape(e.message + '', true)
@@ -122,22 +131,28 @@ export function marked(src, opt, callback) {
     throw e;
   }
 
+  if (opt.async) {
+    return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src)
+      .then(src => Lexer.lex(src, opt))
+      .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens)
+      .then(tokens => Parser.parse(tokens, opt))
+      .then(html => opt.hooks ? opt.hooks.postprocess(html) : html)
+      .catch(onError);
+  }
+
   try {
-    if (opt.async) {
-      let promise = Promise.resolve(Lexer.lex(src, opt));
-      if (opt.walkTokens) {
-        promise = promise.then((tokens) =>
-          Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens)
-        );
-      }
-      return promise.then((tokens) => Parser.parse(tokens, opt)).catch(onError);
+    if (opt.hooks) {
+      src = opt.hooks.preprocess(src);
     }
-
     const tokens = Lexer.lex(src, opt);
     if (opt.walkTokens) {
       marked.walkTokens(tokens, opt.walkTokens);
     }
-    return Parser.parse(tokens, opt);
+    let html = Parser.parse(tokens, opt);
+    if (opt.hooks) {
+      html = opt.hooks.postprocess(html);
+    }
+    return html;
   } catch (e) {
     return onError(e);
   }
@@ -170,7 +185,7 @@ marked.use = function(...args) {
     const opts = merge({}, pack);
 
     // set async to true if it was set to true before
-    opts.async = marked.defaults.async || opts.async;
+    opts.async = marked.defaults.async || opts.async || false;
 
     // ==-- Parse "addon" extensions --== //
     if (pack.extensions) {
@@ -257,6 +272,35 @@ marked.use = function(...args) {
       opts.tokenizer = tokenizer;
     }
 
+    // ==-- Parse Hooks extensions --== //
+    if (pack.hooks) {
+      const hooks = marked.defaults.hooks || new Hooks();
+      for (const prop in pack.hooks) {
+        const prevHook = hooks[prop];
+        if (Hooks.passThroughHooks.has(prop)) {
+          hooks[prop] = (arg) => {
+            if (marked.defaults.async) {
+              return Promise.resolve(pack.hooks[prop].call(marked, arg)).then(ret => {
+                return prevHook.call(marked, ret);
+              });
+            }
+
+            const ret = pack.hooks[prop].call(marked, arg);
+            return prevHook.call(marked, ret);
+          };
+        } else {
+          hooks[prop] = (...args) => {
+            let ret = pack.hooks[prop].apply(marked, args);
+            if (ret === false) {
+              ret = prevHook.apply(marked, args);
+            }
+            return ret;
+          };
+        }
+      }
+      opts.hooks = hooks;
+    }
+
     // ==-- Parse WalkTokens extensions --== //
     if (pack.walkTokens) {
       const walkTokens = marked.defaults.walkTokens;
@@ -357,6 +401,7 @@ marked.Lexer = Lexer;
 marked.lexer = Lexer.lex;
 marked.Tokenizer = Tokenizer;
 marked.Slugger = Slugger;
+marked.Hooks = Hooks;
 marked.parse = marked;
 
 export const options = marked.options;
@@ -374,3 +419,4 @@ export { Tokenizer } from './Tokenizer.js';
 export { Renderer } from './Renderer.js';
 export { TextRenderer } from './TextRenderer.js';
 export { Slugger } from './Slugger.js';
+export { Hooks } from './Hooks.js';

From ccb91d9c4bd8e2f2c0e687a9cf72cabb23a25eaa Mon Sep 17 00:00:00 2001
From: Tony Brix 
Date: Sat, 11 Feb 2023 09:45:47 -0600
Subject: [PATCH 02/13] add tests

---
 test/unit/marked-spec.js | 99 ++++++++++++++++++++++++++++++++++++++--
 1 file changed, 95 insertions(+), 4 deletions(-)

diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js
index 4dab6aeaac..21132fd424 100644
--- a/test/unit/marked-spec.js
+++ b/test/unit/marked-spec.js
@@ -1,5 +1,11 @@
 import { marked, Renderer, Slugger, lexer, parseInline, use, getDefaults, walkTokens as _walkTokens, defaults, setOptions } from '../../src/marked.js';
 
+async function timeout(ms = 1) {
+  return new Promise(resolve => {
+    setTimeout(resolve, ms);
+  });
+}
+
 describe('Test heading ID functionality', () => {
   it('should add id attribute by default', () => {
     const renderer = new Renderer();
@@ -1099,9 +1105,7 @@ br
       async: true,
       async walkTokens(token) {
         if (token.type === 'em') {
-          await new Promise((resolve) => {
-            setTimeout(resolve, 100);
-          });
+          await timeout();
           token.text += ' walked';
           token.tokens = this.Lexer.lexInline(token.text);
         }
@@ -1113,7 +1117,7 @@ br
     expect(html.trim()).toBe('

text walked

'); }); - it('should return promise if async', async() => { + it('should return promise if async and no walkTokens function', async() => { marked.use({ async: true }); @@ -1123,3 +1127,90 @@ br expect(html.trim()).toBe('

text

'); }); }); + +describe('Hooks', () => { + it('should preprocess markdown', () => { + marked.use({ + hooks: { + preprocess(markdown) { + return `# preprocess\n\n${markdown}`; + } + } + }); + const html = marked('*text*'); + expect(html.trim()).toBe('

preprocess

\n

text

'); + }); + + it('should preprocess async', async() => { + marked.use({ + async: true, + hooks: { + async preprocess(markdown) { + await timeout(); + return `# preprocess async\n\n${markdown}`; + } + } + }); + const promise = marked('*text*'); + expect(promise).toBeInstanceOf(Promise); + const html = await promise; + expect(html.trim()).toBe('

preprocess async

\n

text

'); + }); + + it('should postprocess html', () => { + marked.use({ + hooks: { + postprocess(html) { + return html + '

postprocess

'; + } + } + }); + const html = marked('*text*'); + expect(html.trim()).toBe('

text

\n

postprocess

'); + }); + + it('should postprocess async', async() => { + marked.use({ + async: true, + hooks: { + async postprocess(html) { + await timeout(); + return html + '

postprocess async

\n'; + } + } + }); + const promise = marked('*text*'); + expect(promise).toBeInstanceOf(Promise); + const html = await promise; + expect(html.trim()).toBe('

text

\n

postprocess async

'); + }); + + it('should process all hooks in reverse', async() => { + marked.use({ + hooks: { + preprocess(markdown) { + return `# preprocess1\n\n${markdown}`; + }, + postprocess(html) { + return html + '

postprocess1

\n'; + } + } + }); + marked.use({ + async: true, + hooks: { + preprocess(markdown) { + return `# preprocess2\n\n${markdown}`; + }, + async postprocess(html) { + await timeout(); + return html + '

postprocess2 async

\n'; + } + } + }); + const promise = marked('*text*'); + expect(promise).toBeInstanceOf(Promise); + const html = await promise; + expect(html.trim()).toBe('

preprocess1

\n

preprocess2

\n

text

\n

postprocess2 async

\n

postprocess1

'); + }); +}); From 3b9d344851c4e798bf8e68fe89b8e9bd3fcd2578 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 10:40:01 -0600 Subject: [PATCH 03/13] add docs --- docs/USING_PRO.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index c4757e0291..53a6cdf3c7 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -293,6 +293,62 @@ console.log(marked.parse('# heading 2\n\n## heading 3')); *** +

Hooks : hooks

+ +Hooks are methods that hook into some part of marked. The following hooks are available: + +| signature | description | +|------|-------------|-----------| +| `preprocess(markdown : string) : string` | Process markdown before sending it to marked. | +| `postprocess(html : string) : string` | Process html after marked has finished parsing. | + +The `this` variable in the hooks is the `marked` object. + +`marked.use()` can be called multiple times with different `hooks` functions. Each function will be called in order, starting with the function that was assigned *last*. + +**Example:** Set options based on [front-matter](https://www.npmjs.com/package/front-matter) + +```js +import { marked } from 'marked'; +import fm from 'front-matter'; + +let defaults; +// Override function +const hooks = { + preprocess(markdown) { + // shallow copy options for reset; + defaults = { ...this.defaults }; + const { attributes, body } = fm(markdown); + this.setOptions(attributes); + return body; + }, + postprocess(html) { + // reset options + this.setOptions(defaults); + return html; + } +}; + +marked.use({ hooks }); + +// Run marked +console.log(marked.parse(` +--- +headerIds: false +--- + +## test +`.trim())); +``` + +**Output:** + +```html +

test

+``` + +*** +

Custom Extensions : extensions

You may supply an `extensions` array to the `options` object. This array can contain any number of `extension` objects, using the following properties: From 36051ca8fa8af3877ca202bb0b82a21ea3e141cf Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 10:40:11 -0600 Subject: [PATCH 04/13] update src --- src/helpers.js | 17 ----------------- src/marked.js | 17 +++++++++++------ src/rules.js | 32 ++++++++++++++++++-------------- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index 711b5d1959..df7b0bb255 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -142,23 +142,6 @@ export function resolveUrl(base, href) { export const noopTest = { exec: function noopTest() {} }; -export function merge(obj) { - let i = 1, - target, - key; - - for (; i < arguments.length; i++) { - target = arguments[i]; - for (key in target) { - if (Object.prototype.hasOwnProperty.call(target, key)) { - obj[key] = target[key]; - } - } - } - - return obj; -} - export function splitCells(tableRow, count) { // ensure that every cell-delimiting pipe has a space // before it to distinguish it from an escaped pipe diff --git a/src/marked.js b/src/marked.js index d80bc5a011..4bb12dd65d 100644 --- a/src/marked.js +++ b/src/marked.js @@ -6,7 +6,6 @@ import { TextRenderer } from './TextRenderer.js'; import { Slugger } from './Slugger.js'; import { Hooks } from './Hooks.js'; import { - merge, checkSanitizeDeprecation, escape } from './helpers.js'; @@ -34,17 +33,18 @@ export function marked(src, opt, callback) { opt = null; } - opt = merge({}, marked.defaults, opt || {}); + const origOpt = { ...opt }; + opt = { ...marked.defaults, ...origOpt }; checkSanitizeDeprecation(opt); if (callback) { - marked.setOptions({ async: false }); const highlight = opt.highlight; let tokens; try { if (opt.hooks) { src = opt.hooks.preprocess(src); + opt = { ...marked.defaults, ...origOpt }; } tokens = Lexer.lex(src, opt); } catch (e) { @@ -133,6 +133,10 @@ export function marked(src, opt, callback) { if (opt.async) { return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => { + opt = { ...marked.defaults, ...origOpt }; + return src; + }) .then(src => Lexer.lex(src, opt)) .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) .then(tokens => Parser.parse(tokens, opt)) @@ -143,6 +147,7 @@ export function marked(src, opt, callback) { try { if (opt.hooks) { src = opt.hooks.preprocess(src); + opt = { ...marked.defaults, ...origOpt }; } const tokens = Lexer.lex(src, opt); if (opt.walkTokens) { @@ -164,7 +169,7 @@ export function marked(src, opt, callback) { marked.options = marked.setOptions = function(opt) { - merge(marked.defaults, opt); + marked.defaults = { ...marked.defaults, ...opt }; changeDefaults(marked.defaults); return marked; }; @@ -182,7 +187,7 @@ marked.use = function(...args) { args.forEach((pack) => { // copy options to new object - const opts = merge({}, pack); + const opts = { ...pack }; // set async to true if it was set to true before opts.async = marked.defaults.async || opts.async || false; @@ -370,7 +375,7 @@ marked.parseInline = function(src, opt) { + Object.prototype.toString.call(src) + ', string expected'); } - opt = merge({}, marked.defaults, opt || {}); + opt = { ...marked.defaults, ...opt }; checkSanitizeDeprecation(opt); try { diff --git a/src/rules.js b/src/rules.js index c0e763acfc..322825bbcb 100644 --- a/src/rules.js +++ b/src/rules.js @@ -1,7 +1,6 @@ import { noopTest, - edit, - merge + edit } from './helpers.js'; /** @@ -85,17 +84,18 @@ block.blockquote = edit(block.blockquote) * Normal Block Grammar */ -block.normal = merge({}, block); +block.normal = { ...block }; /** * GFM Block Grammar */ -block.gfm = merge({}, block.normal, { +block.gfm = { + ...block.normal, table: '^ *([^\\n ].*\\|.*)\\n' // Header + ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' // Align + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)' // Cells -}); +}; block.gfm.table = edit(block.gfm.table) .replace('hr', block.hr) @@ -123,7 +123,8 @@ block.gfm.paragraph = edit(block._paragraph) * Pedantic grammar (original John Gruber's loose markdown specification) */ -block.pedantic = merge({}, block.normal, { +block.pedantic = { + ...block.normal, html: edit( '^ *(?:comment *(?:\\n|\\s*$)' + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag @@ -147,7 +148,7 @@ block.pedantic = merge({}, block.normal, { .replace('|list', '') .replace('|html', '') .getRegex() -}); +}; /** * Inline-Level Grammar @@ -249,13 +250,14 @@ inline.reflinkSearch = edit(inline.reflinkSearch, 'g') * Normal Inline Grammar */ -inline.normal = merge({}, inline); +inline.normal = { ...inline }; /** * Pedantic Inline Grammar */ -inline.pedantic = merge({}, inline.normal, { +inline.pedantic = { + ...inline.normal, strong: { start: /^__|\*\*/, middle: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, @@ -274,20 +276,21 @@ inline.pedantic = merge({}, inline.normal, { reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) .replace('label', inline._label) .getRegex() -}); +}; /** * GFM Inline Grammar */ -inline.gfm = merge({}, inline.normal, { +inline.gfm = { + ...inline.normal, escape: edit(inline.escape).replace('])', '~|])').getRegex(), _extended_email: /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/, text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\ Date: Sat, 11 Feb 2023 10:59:05 -0600 Subject: [PATCH 05/13] add options tests --- test/unit/marked-spec.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 21132fd424..1c6cf11d1e 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -1157,6 +1157,34 @@ describe('Hooks', () => { expect(html.trim()).toBe('

preprocess async

\n

text

'); }); + it('should preprocess options', () => { + marked.use({ + hooks: { + preprocess(markdown) { + this.setOptions({ headerIds: false }); + return markdown; + } + } + }); + const html = marked('# test'); + expect(html.trim()).toBe('

test

'); + }); + + it('should preprocess options async', async() => { + marked.use({ + async: true, + hooks: { + async preprocess(markdown) { + await timeout(); + this.setOptions({ headerIds: false }); + return markdown; + } + } + }); + const html = await marked('# test'); + expect(html.trim()).toBe('

test

'); + }); + it('should postprocess html', () => { marked.use({ hooks: { From 91fdfd08f72094a55bdcca5486311b5ec7c31e8e Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 11:12:06 -0600 Subject: [PATCH 06/13] add docs nav --- docs/_document.html | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_document.html b/docs/_document.html index 6967f088d5..4fc452a8e1 100644 --- a/docs/_document.html +++ b/docs/_document.html @@ -53,6 +53,7 @@

Marked Documentation

  • Renderer
  • Tokenizer
  • Walk Tokens
  • +
  • Hooks
  • Custom Extensions
  • Async Marked
  • Lexer
  • From e5192ab5af20b227e747a5b8705fa6aa10ba980a Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 11:13:02 -0600 Subject: [PATCH 07/13] fix docs --- docs/USING_PRO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index 53a6cdf3c7..47d90517fe 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -298,7 +298,7 @@ console.log(marked.parse('# heading 2\n\n## heading 3')); Hooks are methods that hook into some part of marked. The following hooks are available: | signature | description | -|------|-------------|-----------| +|-----------|-------------| | `preprocess(markdown : string) : string` | Process markdown before sending it to marked. | | `postprocess(html : string) : string` | Process html after marked has finished parsing. | From 46598cec019190d7a16c29db5a6cc7dccf1504cb Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 11:14:21 -0600 Subject: [PATCH 08/13] fix signature --- docs/USING_PRO.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index 47d90517fe..f7453ca6b4 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -299,8 +299,8 @@ Hooks are methods that hook into some part of marked. The following hooks are av | signature | description | |-----------|-------------| -| `preprocess(markdown : string) : string` | Process markdown before sending it to marked. | -| `postprocess(html : string) : string` | Process html after marked has finished parsing. | +| `preprocess(markdown: string): string` | Process markdown before sending it to marked. | +| `postprocess(html: string): string` | Process html after marked has finished parsing. | The `this` variable in the hooks is the `marked` object. From d3bca2fd49a98fee4cec021b401d351730c49c90 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 12:49:40 -0600 Subject: [PATCH 09/13] clean up --- docs/USING_PRO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index f7453ca6b4..521db1cd30 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -262,7 +262,7 @@ smartypants('"this ... string"')

    Walk Tokens : walkTokens

    -The walkTokens function gets called with every token. Child tokens are called before moving on to sibling tokens. Each token is passed by reference so updates are persisted when passed to the parser. When [`async`](#async) mode is enabled, the return value is awaited. Otherwise the return value is ignored. +The walkTokens function gets called with every token. Child tokens are called before moving on to sibling tokens. Each token is passed by reference so updates are persisted when passed to the parser. When [`async`](#async) mode is enabled, the return value is awaited. Otherwise the return value is ignored. `marked.use()` can be called multiple times with different `walkTokens` functions. Each function will be called in order, starting with the function that was assigned *last*. From a69cdbc0d1cfd42cf36bbe08eeafef44b0e4627e Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 23:35:52 -0600 Subject: [PATCH 10/13] use hooks options --- docs/USING_PRO.md | 41 +++++++++++++++++----- src/Hooks.js | 6 ++++ src/marked.js | 76 +++++++++++++++++++++++++++++----------- test/unit/marked-spec.js | 72 +++++++++++++++++++++++++------------ 4 files changed, 144 insertions(+), 51 deletions(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index 521db1cd30..045a7979bf 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -312,20 +312,16 @@ The `this` variable in the hooks is the `marked` object. import { marked } from 'marked'; import fm from 'front-matter'; -let defaults; // Override function const hooks = { preprocess(markdown) { - // shallow copy options for reset; - defaults = { ...this.defaults }; const { attributes, body } = fm(markdown); - this.setOptions(attributes); + for (const prop in attributes) { + if (prop in this.options) { + this.options[prop] = attributes[prop]; + } + } return body; - }, - postprocess(html) { - // reset options - this.setOptions(defaults); - return html; } }; @@ -347,6 +343,33 @@ headerIds: false

    test

    ``` +**Example:** Sanitize HTML with [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) + +```js +import { marked } from 'marked'; +import DOMPurify from 'isomorphic-dompurify'; + +// Override function +const hooks = { + postprocess(html) { + return DOMPurify.sanitize(html); + } +}; + +marked.use({ hooks }); + +// Run marked +console.log(marked.parse(` + +`)); +``` + +**Output:** + +```html +

    test

    +``` + ***

    Custom Extensions : extensions

    diff --git a/src/Hooks.js b/src/Hooks.js index 5ceae59a74..af4bb2fbfe 100644 --- a/src/Hooks.js +++ b/src/Hooks.js @@ -1,4 +1,10 @@ +import { defaults } from './defaults.js'; + export class Hooks { + constructor(options) { + this.options = options || defaults; + } + static passThroughHooks = new Set([ 'preprocess', 'postprocess' diff --git a/src/marked.js b/src/marked.js index 4bb12dd65d..eeb2ef9dbb 100644 --- a/src/marked.js +++ b/src/marked.js @@ -37,6 +37,10 @@ export function marked(src, opt, callback) { opt = { ...marked.defaults, ...origOpt }; checkSanitizeDeprecation(opt); + if (opt.hooks) { + opt.hooks.options = opt; + } + if (callback) { const highlight = opt.highlight; let tokens; @@ -44,7 +48,6 @@ export function marked(src, opt, callback) { try { if (opt.hooks) { src = opt.hooks.preprocess(src); - opt = { ...marked.defaults, ...origOpt }; } tokens = Lexer.lex(src, opt); } catch (e) { @@ -133,10 +136,6 @@ export function marked(src, opt, callback) { if (opt.async) { return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) - .then(src => { - opt = { ...marked.defaults, ...origOpt }; - return src; - }) .then(src => Lexer.lex(src, opt)) .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) .then(tokens => Parser.parse(tokens, opt)) @@ -147,7 +146,6 @@ export function marked(src, opt, callback) { try { if (opt.hooks) { src = opt.hooks.preprocess(src); - opt = { ...marked.defaults, ...origOpt }; } const tokens = Lexer.lex(src, opt); if (opt.walkTokens) { @@ -285,19 +283,19 @@ marked.use = function(...args) { if (Hooks.passThroughHooks.has(prop)) { hooks[prop] = (arg) => { if (marked.defaults.async) { - return Promise.resolve(pack.hooks[prop].call(marked, arg)).then(ret => { - return prevHook.call(marked, ret); + return Promise.resolve(pack.hooks[prop].call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); }); } - const ret = pack.hooks[prop].call(marked, arg); - return prevHook.call(marked, ret); + const ret = pack.hooks[prop].call(hooks, arg); + return prevHook.call(hooks, ret); }; } else { hooks[prop] = (...args) => { - let ret = pack.hooks[prop].apply(marked, args); + let ret = pack.hooks[prop].apply(hooks, args); if (ret === false) { - ret = prevHook.apply(marked, args); + ret = prevHook.apply(hooks, args); } return ret; }; @@ -378,21 +376,59 @@ marked.parseInline = function(src, opt) { opt = { ...marked.defaults, ...opt }; checkSanitizeDeprecation(opt); - try { - const tokens = Lexer.lexInline(src, opt); - if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens); - } - return Parser.parseInline(tokens, opt); - } catch (e) { + if (opt.hooks) { + opt.hooks.options = opt; + } + + function onError(e) { e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (opt.silent) { - return '

    An error occurred:

    '
    +      const msg = '

    An error occurred:

    '
             + escape(e.message + '', true)
             + '
    '; + if (opt.async) { + return Promise.resolve(msg); + } + return msg; + } + + if (opt.async) { + return Promise.reject(e); } throw e; } + + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(([src, newOpts]) => { + opt = newOpts; + return src; + }) + .then(src => Lexer.lexInline(src, opt)) + .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => Parser.parseInline(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .then(([html]) => html) + .catch(onError); + } + + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + const tokens = Lexer.lexInline(src, opt); + if (opt.walkTokens) { + marked.walkTokens(tokens, opt.walkTokens); + } + let html = Parser.parseInline(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } catch (e) { + return onError(e); + } }; /** diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 1c6cf11d1e..84cec31d7f 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -1132,8 +1132,11 @@ describe('Hooks', () => { it('should preprocess markdown', () => { marked.use({ hooks: { - preprocess(markdown) { - return `# preprocess\n\n${markdown}`; + preprocess([markdown, options]) { + return [ + `# preprocess\n\n${markdown}`, + options + ]; } } }); @@ -1145,9 +1148,12 @@ describe('Hooks', () => { marked.use({ async: true, hooks: { - async preprocess(markdown) { + async preprocess([markdown, options]) { await timeout(); - return `# preprocess async\n\n${markdown}`; + return [ + `# preprocess async\n\n${markdown}`, + options + ]; } } }); @@ -1160,9 +1166,11 @@ describe('Hooks', () => { it('should preprocess options', () => { marked.use({ hooks: { - preprocess(markdown) { - this.setOptions({ headerIds: false }); - return markdown; + preprocess([markdown, options]) { + return [ + markdown, + { ...options, headerIds: false } + ]; } } }); @@ -1174,10 +1182,12 @@ describe('Hooks', () => { marked.use({ async: true, hooks: { - async preprocess(markdown) { + async preprocess([markdown, options]) { await timeout(); - this.setOptions({ headerIds: false }); - return markdown; + return [ + markdown, + { ...options, headerIds: false } + ]; } } }); @@ -1188,8 +1198,11 @@ describe('Hooks', () => { it('should postprocess html', () => { marked.use({ hooks: { - postprocess(html) { - return html + '

    postprocess

    '; + postprocess([html, options]) { + return [ + html + '

    postprocess

    ', + options + ]; } } }); @@ -1201,9 +1214,12 @@ describe('Hooks', () => { marked.use({ async: true, hooks: { - async postprocess(html) { + async postprocess([html, options]) { await timeout(); - return html + '

    postprocess async

    \n'; + return [ + html + '

    postprocess async

    \n', + options + ]; } } }); @@ -1216,23 +1232,35 @@ describe('Hooks', () => { it('should process all hooks in reverse', async() => { marked.use({ hooks: { - preprocess(markdown) { - return `# preprocess1\n\n${markdown}`; + preprocess([markdown, options]) { + return [ + `# preprocess1\n\n${markdown}`, + options + ]; }, - postprocess(html) { - return html + '

    postprocess1

    \n'; + postprocess([html, options]) { + return [ + html + '

    postprocess1

    \n', + options + ]; } } }); marked.use({ async: true, hooks: { - preprocess(markdown) { - return `# preprocess2\n\n${markdown}`; + preprocess([markdown, options]) { + return [ + `# preprocess2\n\n${markdown}`, + options + ]; }, - async postprocess(html) { + async postprocess([html, options]) { await timeout(); - return html + '

    postprocess2 async

    \n'; + return [ + html + '

    postprocess2 async

    \n', + options + ]; } } }); From c6b23482cfefb06825e588174ee557e974878ca2 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 23:39:03 -0600 Subject: [PATCH 11/13] fix output --- docs/USING_PRO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index 045a7979bf..54d3330a7d 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -367,7 +367,7 @@ console.log(marked.parse(` **Output:** ```html -

    test

    + ``` *** From 17af7e3720fea3857a6cf61b02f05963d86e037d Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sat, 11 Feb 2023 23:43:30 -0600 Subject: [PATCH 12/13] fix tests --- docs/USING_PRO.md | 2 -- src/marked.js | 5 --- test/unit/marked-spec.js | 72 ++++++++++++---------------------------- 3 files changed, 22 insertions(+), 57 deletions(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index 54d3330a7d..dbb8e7a910 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -302,8 +302,6 @@ Hooks are methods that hook into some part of marked. The following hooks are av | `preprocess(markdown: string): string` | Process markdown before sending it to marked. | | `postprocess(html: string): string` | Process html after marked has finished parsing. | -The `this` variable in the hooks is the `marked` object. - `marked.use()` can be called multiple times with different `hooks` functions. Each function will be called in order, starting with the function that was assigned *last*. **Example:** Set options based on [front-matter](https://www.npmjs.com/package/front-matter) diff --git a/src/marked.js b/src/marked.js index eeb2ef9dbb..ff32b2d7d1 100644 --- a/src/marked.js +++ b/src/marked.js @@ -401,15 +401,10 @@ marked.parseInline = function(src, opt) { if (opt.async) { return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) - .then(([src, newOpts]) => { - opt = newOpts; - return src; - }) .then(src => Lexer.lexInline(src, opt)) .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) .then(tokens => Parser.parseInline(tokens, opt)) .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) - .then(([html]) => html) .catch(onError); } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 84cec31d7f..a4cb681dcb 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -1132,11 +1132,8 @@ describe('Hooks', () => { it('should preprocess markdown', () => { marked.use({ hooks: { - preprocess([markdown, options]) { - return [ - `# preprocess\n\n${markdown}`, - options - ]; + preprocess(markdown) { + return `# preprocess\n\n${markdown}`; } } }); @@ -1148,12 +1145,9 @@ describe('Hooks', () => { marked.use({ async: true, hooks: { - async preprocess([markdown, options]) { + async preprocess(markdown) { await timeout(); - return [ - `# preprocess async\n\n${markdown}`, - options - ]; + return `# preprocess async\n\n${markdown}`; } } }); @@ -1166,11 +1160,9 @@ describe('Hooks', () => { it('should preprocess options', () => { marked.use({ hooks: { - preprocess([markdown, options]) { - return [ - markdown, - { ...options, headerIds: false } - ]; + preprocess(markdown) { + this.options.headerIds = false; + return markdown; } } }); @@ -1182,12 +1174,10 @@ describe('Hooks', () => { marked.use({ async: true, hooks: { - async preprocess([markdown, options]) { + async preprocess(markdown) { await timeout(); - return [ - markdown, - { ...options, headerIds: false } - ]; + this.options.headerIds = false; + return markdown; } } }); @@ -1198,11 +1188,8 @@ describe('Hooks', () => { it('should postprocess html', () => { marked.use({ hooks: { - postprocess([html, options]) { - return [ - html + '

    postprocess

    ', - options - ]; + postprocess(html) { + return html + '

    postprocess

    '; } } }); @@ -1214,12 +1201,9 @@ describe('Hooks', () => { marked.use({ async: true, hooks: { - async postprocess([html, options]) { + async postprocess(html) { await timeout(); - return [ - html + '

    postprocess async

    \n', - options - ]; + return html + '

    postprocess async

    \n'; } } }); @@ -1232,35 +1216,23 @@ describe('Hooks', () => { it('should process all hooks in reverse', async() => { marked.use({ hooks: { - preprocess([markdown, options]) { - return [ - `# preprocess1\n\n${markdown}`, - options - ]; + preprocess(markdown) { + return `# preprocess1\n\n${markdown}`; }, - postprocess([html, options]) { - return [ - html + '

    postprocess1

    \n', - options - ]; + postprocess(html) { + return html + '

    postprocess1

    \n'; } } }); marked.use({ async: true, hooks: { - preprocess([markdown, options]) { - return [ - `# preprocess2\n\n${markdown}`, - options - ]; + preprocess(markdown) { + return `# preprocess2\n\n${markdown}`; }, - async postprocess([html, options]) { + async postprocess(html) { await timeout(); - return [ - html + '

    postprocess2 async

    \n', - options - ]; + return html + '

    postprocess2 async

    \n'; } } }); From 199b398b10844b64812f2f95985d71abfa751310 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Thu, 23 Feb 2023 00:41:26 -0600 Subject: [PATCH 13/13] consolidate code --- src/marked.js | 318 ++++++++++++++++++++++---------------------------- 1 file changed, 138 insertions(+), 180 deletions(-) diff --git a/src/marked.js b/src/marked.js index ff32b2d7d1..36ec0403ed 100644 --- a/src/marked.js +++ b/src/marked.js @@ -15,150 +15,169 @@ import { defaults } from './defaults.js'; -/** - * Marked - */ -export function marked(src, opt, callback) { - // throw error in case of non string input - if (typeof src === 'undefined' || src === null) { - throw new Error('marked(): input parameter is undefined or null'); - } - if (typeof src !== 'string') { - throw new Error('marked(): input parameter is of type ' - + Object.prototype.toString.call(src) + ', string expected'); - } - - if (typeof opt === 'function') { - callback = opt; - opt = null; - } - - const origOpt = { ...opt }; - opt = { ...marked.defaults, ...origOpt }; - checkSanitizeDeprecation(opt); - - if (opt.hooks) { - opt.hooks.options = opt; - } - - if (callback) { - const highlight = opt.highlight; - let tokens; +function onError(silent, async, callback) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; - try { - if (opt.hooks) { - src = opt.hooks.preprocess(src); + if (silent) { + const msg = '

    An error occurred:

    '
    +        + escape(e.message + '', true)
    +        + '
    '; + if (async) { + return Promise.resolve(msg); } - tokens = Lexer.lex(src, opt); - } catch (e) { - return callback(e); + if (callback) { + callback(null, msg); + return; + } + return msg; } - const done = function(err) { - let out; + if (async) { + return Promise.reject(e); + } + if (callback) { + callback(e); + return; + } + throw e; + }; +} - if (!err) { - try { - if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens); - } - out = Parser.parse(tokens, opt); - if (opt.hooks) { - out = opt.hooks.postprocess(out); - } - } catch (e) { - err = e; - } - } +function parseMarkdown(lexer, parser) { + return (src, opt, callback) => { + if (typeof opt === 'function') { + callback = opt; + opt = null; + } + + const origOpt = { ...opt }; + opt = { ...marked.defaults, ...origOpt }; + const throwError = onError(opt.silent, opt.async, callback); - opt.highlight = highlight; + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } - return err - ? callback(err) - : callback(null, out); - }; + checkSanitizeDeprecation(opt); - if (!highlight || highlight.length < 3) { - return done(); + if (opt.hooks) { + opt.hooks.options = opt; } - delete opt.highlight; + if (callback) { + const highlight = opt.highlight; + let tokens; - if (!tokens.length) return done(); + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + tokens = lexer(src, opt); + } catch (e) { + return throwError(e); + } + + const done = function(err) { + let out; - let pending = 0; - marked.walkTokens(tokens, function(token) { - if (token.type === 'code') { - pending++; - setTimeout(() => { - highlight(token.text, token.lang, function(err, code) { - if (err) { - return done(err); + if (!err) { + try { + if (opt.walkTokens) { + marked.walkTokens(tokens, opt.walkTokens); } - if (code != null && code !== token.text) { - token.text = code; - token.escaped = true; + out = parser(tokens, opt); + if (opt.hooks) { + out = opt.hooks.postprocess(out); } + } catch (e) { + err = e; + } + } - pending--; - if (pending === 0) { - done(); - } - }); - }, 0); + opt.highlight = highlight; + + return err + ? throwError(err) + : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); } - }); - if (pending === 0) { - done(); - } + delete opt.highlight; - return; - } + if (!tokens.length) return done(); - function onError(e) { - e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + let pending = 0; + marked.walkTokens(tokens, function(token) { + if (token.type === 'code') { + pending++; + setTimeout(() => { + highlight(token.text, token.lang, function(err, code) { + if (err) { + return done(err); + } + if (code != null && code !== token.text) { + token.text = code; + token.escaped = true; + } - if (opt.silent) { - const msg = '

    An error occurred:

    '
    -        + escape(e.message + '', true)
    -        + '
    '; - if (opt.async) { - return Promise.resolve(msg); + pending--; + if (pending === 0) { + done(); + } + }); + }, 0); + } + }); + + if (pending === 0) { + done(); } - return msg; + + return; } + if (opt.async) { - return Promise.reject(e); + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); } - throw e; - } - if (opt.async) { - return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) - .then(src => Lexer.lex(src, opt)) - .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) - .then(tokens => Parser.parse(tokens, opt)) - .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) - .catch(onError); - } - - try { - if (opt.hooks) { - src = opt.hooks.preprocess(src); - } - const tokens = Lexer.lex(src, opt); - if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens); - } - let html = Parser.parse(tokens, opt); - if (opt.hooks) { - html = opt.hooks.postprocess(html); + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + const tokens = lexer(src, opt); + if (opt.walkTokens) { + marked.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } catch (e) { + return throwError(e); } - return html; - } catch (e) { - return onError(e); - } + }; +} + +/** + * Marked + */ +export function marked(src, opt, callback) { + return parseMarkdown(Lexer.lex, Parser.parse)(src, opt, callback); } /** @@ -363,68 +382,7 @@ marked.walkTokens = function(tokens, callback) { * Parse Inline * @param {string} src */ -marked.parseInline = function(src, opt) { - // throw error in case of non string input - if (typeof src === 'undefined' || src === null) { - throw new Error('marked.parseInline(): input parameter is undefined or null'); - } - if (typeof src !== 'string') { - throw new Error('marked.parseInline(): input parameter is of type ' - + Object.prototype.toString.call(src) + ', string expected'); - } - - opt = { ...marked.defaults, ...opt }; - checkSanitizeDeprecation(opt); - - if (opt.hooks) { - opt.hooks.options = opt; - } - - function onError(e) { - e.message += '\nPlease report this to https://github.com/markedjs/marked.'; - - if (opt.silent) { - const msg = '

    An error occurred:

    '
    -        + escape(e.message + '', true)
    -        + '
    '; - if (opt.async) { - return Promise.resolve(msg); - } - return msg; - } - - if (opt.async) { - return Promise.reject(e); - } - throw e; - } - - if (opt.async) { - return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) - .then(src => Lexer.lexInline(src, opt)) - .then(tokens => opt.walkTokens ? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) - .then(tokens => Parser.parseInline(tokens, opt)) - .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) - .catch(onError); - } - - try { - if (opt.hooks) { - src = opt.hooks.preprocess(src); - } - const tokens = Lexer.lexInline(src, opt); - if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens); - } - let html = Parser.parseInline(tokens, opt); - if (opt.hooks) { - html = opt.hooks.postprocess(html); - } - return html; - } catch (e) { - return onError(e); - } -}; +marked.parseInline = parseMarkdown(Lexer.lexInline, Parser.parseInline); /** * Expose