From 04fef1b7d56dc21add62a5dddffa4cdf310aadd4 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 8 May 2021 02:21:12 -0400 Subject: [PATCH 01/48] Initial Commit --- src/Lexer.js | 29 ++++++++++++++++++++++++++++- src/Parser.js | 17 ++++++++++++++++- src/rules.js | 2 +- test/unit/marked-spec.js | 25 +++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index 6c02ed65e5..ab662d1c8a 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -52,6 +52,7 @@ module.exports = class Lexer { this.tokens = []; this.tokens.links = Object.create(null); this.options = options || defaults; + this.options.extensions = this.options.extensions || null; this.options.tokenizer = this.options.tokenizer || new Tokenizer(); this.tokenizer = this.options.tokenizer; this.tokenizer.options = this.options; @@ -101,6 +102,22 @@ module.exports = class Lexer { return lexer.inlineTokens(src); } + runTokenzierExtension(src, tokens, before) { + let tokensLength = 0; + if (this.options.extensions) { + // Find extensions with matching "before" + let token; + Object.values(this.options.extensions).forEach(function(extension, index) { + if (extension.before && extension.before === before && (token = extension.tokenizer(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + tokensLength += token.raw.length; + } + }); + } + return tokensLength; + } + /** * Preprocessing */ @@ -123,9 +140,14 @@ module.exports = class Lexer { if (this.options.pedantic) { src = src.replace(/^ +$/gm, ''); } - let token, i, l, lastToken; + let token, i, l, lastToken, tokensLength; while (src) { + if (this.runTokenzierExtension(src, tokens, 'space')) { + src = src.substring(tokensLength); + continue; + } + // newline if (token = this.tokenizer.space(src)) { src = src.substring(token.raw.length); @@ -229,6 +251,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'paragraph')) { + src = src.substring(tokensLength); + continue; + } + // top-level paragraph if (top && (token = this.tokenizer.paragraph(src))) { src = src.substring(token.raw.length); diff --git a/src/Parser.js b/src/Parser.js index 81fcb7da1e..5bbb5215d7 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -12,6 +12,7 @@ const { module.exports = class Parser { constructor(options) { this.options = options || defaults; + this.options.extensions = this.options.extensions || null; this.options.renderer = this.options.renderer || new Renderer(); this.renderer = this.options.renderer; this.renderer.options = this.options; @@ -57,7 +58,8 @@ module.exports = class Parser { item, checked, task, - checkbox; + checkbox, + tokenParsed; const l = tokens.length; for (i = 0; i < l; i++) { @@ -179,7 +181,20 @@ module.exports = class Parser { out += top ? this.renderer.paragraph(body) : body; continue; } + default: { + // Run any renderer extensions + tokenParsed = false; + if (this.options.extensions) { + Object.values(this.options.extensions).forEach(function(extension, index) { + if (extension.name && extension.name === token.type) { + out += extension.renderer(token); + } + tokenParsed = true; + }); + } + if (tokenParsed) continue; + const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { console.error(errMsg); diff --git a/src/rules.js b/src/rules.js index a06bdb6892..3f57506119 100644 --- a/src/rules.js +++ b/src/rules.js @@ -31,7 +31,7 @@ const block = { lheading: /^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/, // regex template, placeholders will be replaced according to different paragraph // interruption rules of commonmark and the original markdown spec: - _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/, + _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|:| +\n)[^\n]+)*)/, text: /^[^\n]+/ }; diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 86813cc29e..c5994f29e6 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -137,6 +137,31 @@ describe('parseInline', () => { }); describe('use extension', () => { + it('should use full extension', () => { + const underline = { + name: 'underline', + before: 'paragraph', // Leave blank to run after everything else...? + level: 'block', + tokenizer: (src) => { + const rule = /^:([^\n]*)(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'underline', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + }, + renderer: (token) => { + return `${token.text}\n`; + } + }; + marked.use({ extensions: { underline } }); + const html = marked('Not Underlined\n:Underlined\nNot Underlined'); + expect(html).toBe('

Not Underlined

\nUnderlined\n

Not Underlined

\n'); + }); + it('should use renderer', () => { const extension = { renderer: { From 108d4bc434462827c7b84ee59fb9f62dba74a24d Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 8 May 2021 03:13:00 -0400 Subject: [PATCH 02/48] Paragraph Tokenizer handles custom "start" points for extensions --- src/Tokenizer.js | 12 ++++++++++++ src/rules.js | 2 +- test/unit/marked-spec.js | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Tokenizer.js b/src/Tokenizer.js index 33aef2b2b1..7991b87d6b 100644 --- a/src/Tokenizer.js +++ b/src/Tokenizer.js @@ -408,6 +408,18 @@ module.exports = class Tokenizer { } paragraph(src) { + if (this.options.extensions) { + Object.values(this.options.extensions).forEach(function(extension, index) { + if (extension.start) { + // find the next start index + const match = src.match(extension.start); + if (match && match.length > 0) { + // get `src` up to that index + src = src.substring(0, match.index); + } + } + }); + } const cap = this.rules.block.paragraph.exec(src); if (cap) { return { diff --git a/src/rules.js b/src/rules.js index 3f57506119..a06bdb6892 100644 --- a/src/rules.js +++ b/src/rules.js @@ -31,7 +31,7 @@ const block = { lheading: /^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/, // regex template, placeholders will be replaced according to different paragraph // interruption rules of commonmark and the original markdown spec: - _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|:| +\n)[^\n]+)*)/, + _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/, text: /^[^\n]+/ }; diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index c5994f29e6..e32cb81e26 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -142,6 +142,7 @@ describe('use extension', () => { name: 'underline', before: 'paragraph', // Leave blank to run after everything else...? level: 'block', + start: /:/, tokenizer: (src) => { const rule = /^:([^\n]*)(?:\n|$)/; const match = rule.exec(src); From ee563e179878fdeac01b4aaec35d0938e59a8ede Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 8 May 2021 10:06:45 -0400 Subject: [PATCH 03/48] Move "start" logic from Paragraph tokenizer to Lexer --- src/Lexer.js | 17 +++++++++++++++-- src/Tokenizer.js | 12 ------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index ab662d1c8a..242bc32e09 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -140,7 +140,7 @@ module.exports = class Lexer { if (this.options.pedantic) { src = src.replace(/^ +$/gm, ''); } - let token, i, l, lastToken, tokensLength; + let token, i, l, lastToken, tokensLength, cutSrc; while (src) { if (this.runTokenzierExtension(src, tokens, 'space')) { @@ -257,7 +257,20 @@ module.exports = class Lexer { } // top-level paragraph - if (top && (token = this.tokenizer.paragraph(src))) { + cutSrc = src; + if (this.options.extensions) { + Object.values(this.options.extensions).forEach(function(extension, index) { + if (extension.start) { + // find the next start index + const match = src.match(extension.start); + if (match && match.length > 0) { + // get `src` up to that index + cutSrc = src.substring(0, match.index); + } + } + }); + } + if (top && (token = this.tokenizer.paragraph(cutSrc))) { src = src.substring(token.raw.length); tokens.push(token); continue; diff --git a/src/Tokenizer.js b/src/Tokenizer.js index 7991b87d6b..33aef2b2b1 100644 --- a/src/Tokenizer.js +++ b/src/Tokenizer.js @@ -408,18 +408,6 @@ module.exports = class Tokenizer { } paragraph(src) { - if (this.options.extensions) { - Object.values(this.options.extensions).forEach(function(extension, index) { - if (extension.start) { - // find the next start index - const match = src.match(extension.start); - if (match && match.length > 0) { - // get `src` up to that index - src = src.substring(0, match.index); - } - } - }); - } const cap = this.rules.block.paragraph.exec(src); if (cap) { return { From a0798c3b4cc8df4fd6da17515284ded2e669a33c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 8 May 2021 13:26:43 -0400 Subject: [PATCH 04/48] Clear extensions after each "Use Extension" unit test --- test/unit/marked-spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index e32cb81e26..3c731dae0a 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -137,7 +137,11 @@ describe('parseInline', () => { }); describe('use extension', () => { - it('should use full extension', () => { + afterEach(function() { + marked.defaults = marked.getDefaults(); + }); + + it('should use block tokenizer + renderer extensions', () => { const underline = { name: 'underline', before: 'paragraph', // Leave blank to run after everything else...? From 1308bc36a825a58381ba998586d33afdb1dad0c8 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 8 May 2021 13:37:25 -0400 Subject: [PATCH 05/48] Unit test: require blank lines for custom block tokens if not using "start" property --- test/unit/marked-spec.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 3c731dae0a..aeb4f384ee 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -138,10 +138,38 @@ describe('parseInline', () => { describe('use extension', () => { afterEach(function() { - marked.defaults = marked.getDefaults(); + // marked.defaults = marked.getDefaults(); // <- This is causing tests in parser-spec.js to fail? What? }); - it('should use block tokenizer + renderer extensions', () => { + it('should use custom block tokenizer + renderer extensions', () => { + const underline = { + name: 'underline', + before: 'paragraph', // Leave blank to run after everything else...? + level: 'block', + tokenizer: (src) => { + const rule = /^:([^\n]*)(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'underline', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + }, + renderer: (token) => { + return `${token.text}\n`; + } + }; + marked.use({ extensions: { underline } }); + let html = marked('Not Underlined\n:Underlined\nNot Underlined'); + expect(html).toBe('

Not Underlined\n:Underlined\nNot Underlined

\n'); + + html = marked('Not Underlined\n\n:Underlined\n\nNot Underlined'); + expect(html).toBe('

Not Underlined

\nUnderlined\n

Not Underlined

\n'); + }); + + it('should interrupt paragraphs if using "start" property', () => { const underline = { name: 'underline', before: 'paragraph', // Leave blank to run after everything else...? From 803bbac0a069cbfb1d18ba4405fa8884e8652497 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 8 May 2021 22:03:23 -0400 Subject: [PATCH 06/48] Handle inline tokens --- src/Lexer.js | 26 ++++++++++++++++++++++---- src/Parser.js | 15 ++++++++++++++- test/unit/marked-spec.js | 26 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index 242bc32e09..e3b36a0c2e 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -260,7 +260,7 @@ module.exports = class Lexer { cutSrc = src; if (this.options.extensions) { Object.values(this.options.extensions).forEach(function(extension, index) { - if (extension.start) { + if (extension.level === 'block' && extension.start) { // find the next start index const match = src.match(extension.start); if (match && match.length > 0) { @@ -372,12 +372,12 @@ module.exports = class Lexer { * Lexing/Compiling */ inlineTokens(src, tokens = [], inLink = false, inRawBlock = false) { - let token, lastToken; + let token, lastToken, cutSrc; // String with links masked to avoid interference with em and strong let maskedSrc = src; let match; - let keepPrevChar, prevChar; + let keepPrevChar, prevChar, tokensLength; // Mask out reflinks if (this.tokens.links) { @@ -454,6 +454,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'emStrong')) { + src = src.substring(tokensLength); + continue; + } + // em & strong if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { src = src.substring(token.raw.length); @@ -499,7 +504,20 @@ module.exports = class Lexer { } // text - if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) { + cutSrc = src; + if (this.options.extensions) { + Object.values(this.options.extensions).forEach(function(extension, index) { + if (extension.level === 'inline' && extension.start) { + // find the next start index + const match = src.match(extension.start); + if (match && match.length > 0) { + // get `src` up to that index + cutSrc = src.substring(0, match.index); + } + } + }); + } + if (token = this.tokenizer.inlineText(cutSrc, inRawBlock, smartypants)) { src = src.substring(token.raw.length); if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started prevChar = token.raw.slice(-1); diff --git a/src/Parser.js b/src/Parser.js index 5bbb5215d7..5ec2ed0046 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -216,7 +216,8 @@ module.exports = class Parser { renderer = renderer || this.renderer; let out = '', i, - token; + token, + tokenParsed; const l = tokens.length; for (i = 0; i < l; i++) { @@ -263,6 +264,18 @@ module.exports = class Parser { break; } default: { + // Run any renderer extensions + tokenParsed = false; + if (this.options.extensions) { + Object.values(this.options.extensions).forEach(function(extension, index) { + if (extension.name && extension.name === token.type) { + out += extension.renderer(token); + } + tokenParsed = true; + }); + } + if (tokenParsed) continue; + const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { console.error(errMsg); diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index aeb4f384ee..fc7bc1f602 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -195,6 +195,32 @@ describe('use extension', () => { expect(html).toBe('

Not Underlined

\nUnderlined\n

Not Underlined

\n'); }); + it('should use custom inline tokenizer + renderer extensions', () => { + const underline = { + name: 'underline', + before: 'emStrong', // Leave blank to run after everything else...? + level: 'inline', + start: /=/, + tokenizer: (src) => { + const rule = /^=([^=]+)=/; + const match = rule.exec(src); + if (match) { + return { + type: 'underline', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + }, + renderer: (token) => { + return `${token.text}`; + } + }; + marked.use({ extensions: { underline } }); + const html = marked('Not Underlined =Underlined= Not Underlined'); + expect(html).toBe('

Not Underlined Underlined Not Underlined

\n'); + }); + it('should use renderer', () => { const extension = { renderer: { From 94befe4e2db1a78d24861d17f15410a77d9e9200 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 10 May 2021 23:49:32 -0400 Subject: [PATCH 07/48] Put extensions in map to prevent looping --- src/Lexer.js | 41 ++++++--------- src/Parser.js | 20 +++---- src/defaults.js | 1 + src/marked.js | 110 +++++++++++++++++++++++++++------------ test/unit/marked-spec.js | 17 +++--- 5 files changed, 110 insertions(+), 79 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index e3b36a0c2e..500e827aab 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -104,11 +104,10 @@ module.exports = class Lexer { runTokenzierExtension(src, tokens, before) { let tokensLength = 0; - if (this.options.extensions) { - // Find extensions with matching "before" + if (this.options.extensions && this.options.extensions[before]) { let token; - Object.values(this.options.extensions).forEach(function(extension, index) { - if (extension.before && extension.before === before && (token = extension.tokenizer(src))) { + this.options.extensions[before].forEach(function(extTokenizer, index) { + if (token = extTokenizer(src)) { src = src.substring(token.raw.length); tokens.push(token); tokensLength += token.raw.length; @@ -258,17 +257,12 @@ module.exports = class Lexer { // top-level paragraph cutSrc = src; - if (this.options.extensions) { - Object.values(this.options.extensions).forEach(function(extension, index) { - if (extension.level === 'block' && extension.start) { - // find the next start index - const match = src.match(extension.start); - if (match && match.length > 0) { - // get `src` up to that index - cutSrc = src.substring(0, match.index); - } - } - }); + // find the next extension start, and clip 'src' up to that index + if (this.options.extensions && this.options.extensions.startBlock) { + const match = src.match(this.options.extensions.startBlock); + if (match && match.length > 0) { + cutSrc = src.substring(0, match.index); + } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { src = src.substring(token.raw.length); @@ -505,17 +499,12 @@ module.exports = class Lexer { // text cutSrc = src; - if (this.options.extensions) { - Object.values(this.options.extensions).forEach(function(extension, index) { - if (extension.level === 'inline' && extension.start) { - // find the next start index - const match = src.match(extension.start); - if (match && match.length > 0) { - // get `src` up to that index - cutSrc = src.substring(0, match.index); - } - } - }); + // find the next extension start, and clip 'src' up to that index + if (this.options.extensions && this.options.extensions.startInline) { + const match = src.match(this.options.extensions.startInline); + if (match && match.length > 0) { + cutSrc = src.substring(0, match.index); + } } if (token = this.tokenizer.inlineText(cutSrc, inRawBlock, smartypants)) { src = src.substring(token.raw.length); diff --git a/src/Parser.js b/src/Parser.js index 5ec2ed0046..28b5ec6a17 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -185,13 +185,9 @@ module.exports = class Parser { default: { // Run any renderer extensions tokenParsed = false; - if (this.options.extensions) { - Object.values(this.options.extensions).forEach(function(extension, index) { - if (extension.name && extension.name === token.type) { - out += extension.renderer(token); - } - tokenParsed = true; - }); + if (this.options.extensions && this.options.extensions[token.type]) { + out += this.options.extensions[token.type](token); + tokenParsed = true; } if (tokenParsed) continue; @@ -266,13 +262,9 @@ module.exports = class Parser { default: { // Run any renderer extensions tokenParsed = false; - if (this.options.extensions) { - Object.values(this.options.extensions).forEach(function(extension, index) { - if (extension.name && extension.name === token.type) { - out += extension.renderer(token); - } - tokenParsed = true; - }); + if (this.options.extensions && this.options.extensions[token.type]) { + out += this.options.extensions[token.type](token); + tokenParsed = true; } if (tokenParsed) continue; diff --git a/src/defaults.js b/src/defaults.js index fe376563da..a4b451fe2f 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -2,6 +2,7 @@ function getDefaults() { return { baseUrl: null, breaks: false, + extensions: null, gfm: true, headerIds: true, headerPrefix: '', diff --git a/src/marked.js b/src/marked.js index 0ba08156af..ccfb9d4583 100644 --- a/src/marked.js +++ b/src/marked.js @@ -142,43 +142,89 @@ marked.defaults = defaults; */ marked.use = function(extension) { - const opts = merge({}, extension); - if (extension.renderer) { - const renderer = marked.defaults.renderer || new Renderer(); - for (const prop in extension.renderer) { - const prevRenderer = renderer[prop]; - renderer[prop] = (...args) => { - let ret = extension.renderer[prop].apply(renderer, args); - if (ret === false) { - ret = prevRenderer.apply(renderer, args); + if (!Array.isArray(extension)) { // Wrap in array if not already to unify processing + extension = [extension]; + } + const opts = merge({}, ...extension); + opts.tokenizer = null; + opts.renderer = null; + const extensions = {}; + + extension.forEach((ext) => { + if (ext.overwrite) { // Handle "overwrite" extensions + if (ext.renderer) { + const renderer = marked.defaults.renderer || new Renderer(); + for (const prop in ext.renderer) { + const prevRenderer = renderer[prop]; + renderer[prop] = (...args) => { + let ret = ext.renderer[prop].apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret; + }; } - return ret; - }; + opts.renderer = renderer; + } + if (ext.tokenizer) { + const tokenizer = marked.defaults.tokenizer || new Tokenizer(); + for (const prop in ext.tokenizer) { + const prevTokenizer = tokenizer[prop]; + tokenizer[prop] = (...args) => { + let ret = ext.tokenizer[prop].apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + } else { // Handle "addon" extensions + if (ext.renderer && ext.name) { + extensions[ext.name] = ext.renderer; + } + if (ext.tokenizer) { + if (ext.start && ext.level) { + if (ext.level === 'block') { + extensions.startBlock = extensions.startBlock + ? new RegExp(extensions.startBlock.source + '|' + ext.start.source) + : ext.start; + } else if (ext.level === 'inline') { + extensions.startInline = extensions.startInline + ? new RegExp(extensions.startInline.source + '|' + ext.start.source) + : ext.start; + } + } + if (ext.before) { + if (extensions[ext.before]) { + extensions[ext.before].push(ext.tokenizer); + } else { + extensions[ext.before] = [ext.tokenizer]; + } + } else { // Handle extensions with no "before" set, will run after all others + if (extensions.last) { + extensions.last.push(ext.tokenizer); + } else { + extensions.last = [ext.tokenizer]; + } + } + } } - opts.renderer = renderer; - } - if (extension.tokenizer) { - const tokenizer = marked.defaults.tokenizer || new Tokenizer(); - for (const prop in extension.tokenizer) { - const prevTokenizer = tokenizer[prop]; - tokenizer[prop] = (...args) => { - let ret = extension.tokenizer[prop].apply(tokenizer, args); - if (ret === false) { - ret = prevTokenizer.apply(tokenizer, args); + + if (ext.walkTokens) { + const walkTokens = marked.defaults.walkTokens; + opts.walkTokens = (token) => { + ext.walkTokens(token); + if (walkTokens) { + walkTokens(token); } - return ret; }; } - opts.tokenizer = tokenizer; - } - if (extension.walkTokens) { - const walkTokens = marked.defaults.walkTokens; - opts.walkTokens = (token) => { - extension.walkTokens(token); - if (walkTokens) { - walkTokens(token); - } - }; + }); + + if (Object.keys(extensions).length) { + opts.extensions = extensions; } marked.setOptions(opts); }; diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index fc7bc1f602..ec25f711ef 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -137,10 +137,6 @@ describe('parseInline', () => { }); describe('use extension', () => { - afterEach(function() { - // marked.defaults = marked.getDefaults(); // <- This is causing tests in parser-spec.js to fail? What? - }); - it('should use custom block tokenizer + renderer extensions', () => { const underline = { name: 'underline', @@ -161,7 +157,7 @@ describe('use extension', () => { return `${token.text}\n`; } }; - marked.use({ extensions: { underline } }); + marked.use(underline); let html = marked('Not Underlined\n:Underlined\nNot Underlined'); expect(html).toBe('

Not Underlined\n:Underlined\nNot Underlined

\n'); @@ -190,7 +186,7 @@ describe('use extension', () => { return `${token.text}\n`; } }; - marked.use({ extensions: { underline } }); + marked.use(underline); const html = marked('Not Underlined\n:Underlined\nNot Underlined'); expect(html).toBe('

Not Underlined

\nUnderlined\n

Not Underlined

\n'); }); @@ -216,13 +212,14 @@ describe('use extension', () => { return `${token.text}`; } }; - marked.use({ extensions: { underline } }); + marked.use(underline); const html = marked('Not Underlined =Underlined= Not Underlined'); expect(html).toBe('

Not Underlined Underlined Not Underlined

\n'); }); it('should use renderer', () => { const extension = { + overwrite: true, renderer: { paragraph(text) { return 'extension'; @@ -238,6 +235,7 @@ describe('use extension', () => { it('should use tokenizer', () => { const extension = { + overwrite: true, tokenizer: { paragraph(text) { return { @@ -315,6 +313,7 @@ describe('use extension', () => { it('should use last extension function and not override others', () => { const extension1 = { + overwrite: true, renderer: { paragraph(text) { return 'extension1 paragraph\n'; @@ -325,6 +324,7 @@ describe('use extension', () => { } }; const extension2 = { + overwrite: true, renderer: { paragraph(text) { return 'extension2 paragraph\n'; @@ -345,6 +345,7 @@ paragraph it('should use previous extension when returning false', () => { const extension1 = { + overwrite: true, renderer: { paragraph(text) { if (text !== 'original') { @@ -355,6 +356,7 @@ paragraph } }; const extension2 = { + overwrite: true, renderer: { paragraph(text) { if (text !== 'extension1' && text !== 'original') { @@ -378,6 +380,7 @@ original it('should get options with this.options', () => { const extension = { + overwrite: true, renderer: { heading: () => { return this.options ? 'arrow options\n' : 'arrow no options\n'; From f4c2b449553ed67ef93cf3551e5db764bd451e95 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 11 May 2021 16:35:07 -0400 Subject: [PATCH 08/48] Remove 'overwrite' keyword / copied runTokenizerExtension between all Tokenizers --- src/Lexer.js | 114 ++++++++++++++++++++++++++++++++++++++- src/marked.js | 105 +++++++++++++++++------------------- test/unit/marked-spec.js | 7 --- 3 files changed, 161 insertions(+), 65 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index 500e827aab..e7efe9a76f 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -156,6 +156,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'code')) { + src = src.substring(tokensLength); + continue; + } + // code if (token = this.tokenizer.code(src)) { src = src.substring(token.raw.length); @@ -170,6 +175,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'fences')) { + src = src.substring(tokensLength); + continue; + } + // fences if (token = this.tokenizer.fences(src)) { src = src.substring(token.raw.length); @@ -177,6 +187,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'heading')) { + src = src.substring(tokensLength); + continue; + } + // heading if (token = this.tokenizer.heading(src)) { src = src.substring(token.raw.length); @@ -184,6 +199,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'nptable')) { + src = src.substring(tokensLength); + continue; + } + // table no leading pipe (gfm) if (token = this.tokenizer.nptable(src)) { src = src.substring(token.raw.length); @@ -191,6 +211,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'hr')) { + src = src.substring(tokensLength); + continue; + } + // hr if (token = this.tokenizer.hr(src)) { src = src.substring(token.raw.length); @@ -198,6 +223,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'blockquote')) { + src = src.substring(tokensLength); + continue; + } + // blockquote if (token = this.tokenizer.blockquote(src)) { src = src.substring(token.raw.length); @@ -206,6 +236,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'list')) { + src = src.substring(tokensLength); + continue; + } + // list if (token = this.tokenizer.list(src)) { src = src.substring(token.raw.length); @@ -217,6 +252,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'html')) { + src = src.substring(tokensLength); + continue; + } + // html if (token = this.tokenizer.html(src)) { src = src.substring(token.raw.length); @@ -224,6 +264,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'def')) { + src = src.substring(tokensLength); + continue; + } + // def if (top && (token = this.tokenizer.def(src))) { src = src.substring(token.raw.length); @@ -236,6 +281,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'table')) { + src = src.substring(tokensLength); + continue; + } + // table (gfm) if (token = this.tokenizer.table(src)) { src = src.substring(token.raw.length); @@ -243,6 +293,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'lheading')) { + src = src.substring(tokensLength); + continue; + } + // lheading if (token = this.tokenizer.lheading(src)) { src = src.substring(token.raw.length); @@ -256,8 +311,8 @@ module.exports = class Lexer { } // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start cutSrc = src; - // find the next extension start, and clip 'src' up to that index if (this.options.extensions && this.options.extensions.startBlock) { const match = src.match(this.options.extensions.startBlock); if (match && match.length > 0) { @@ -270,6 +325,11 @@ module.exports = class Lexer { continue; } + if (this.runTokenzierExtension(src, tokens, 'text')) { + src = src.substring(tokensLength); + continue; + } + // text if (token = this.tokenizer.text(src)) { src = src.substring(token.raw.length); @@ -400,6 +460,11 @@ module.exports = class Lexer { } keepPrevChar = false; + if (tokensLength = this.runTokenzierExtension(src, tokens, 'escape')) { + src = src.substring(tokensLength); + continue; + } + // escape if (token = this.tokenizer.escape(src)) { src = src.substring(token.raw.length); @@ -407,6 +472,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'tag')) { + src = src.substring(tokensLength); + continue; + } + // tag if (token = this.tokenizer.tag(src, inLink, inRawBlock)) { src = src.substring(token.raw.length); @@ -422,6 +492,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'link')) { + src = src.substring(tokensLength); + continue; + } + // link if (token = this.tokenizer.link(src)) { src = src.substring(token.raw.length); @@ -432,6 +507,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'reflink')) { + src = src.substring(tokensLength); + continue; + } + // reflink, nolink if (token = this.tokenizer.reflink(src, this.tokens.links)) { src = src.substring(token.raw.length); @@ -461,6 +541,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'codespan')) { + src = src.substring(tokensLength); + continue; + } + // code if (token = this.tokenizer.codespan(src)) { src = src.substring(token.raw.length); @@ -468,6 +553,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'br')) { + src = src.substring(tokensLength); + continue; + } + // br if (token = this.tokenizer.br(src)) { src = src.substring(token.raw.length); @@ -475,6 +565,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'del')) { + src = src.substring(tokensLength); + continue; + } + // del (gfm) if (token = this.tokenizer.del(src)) { src = src.substring(token.raw.length); @@ -483,6 +578,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'autolink')) { + src = src.substring(tokensLength); + continue; + } + // autolink if (token = this.tokenizer.autolink(src, mangle)) { src = src.substring(token.raw.length); @@ -490,6 +590,11 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'url')) { + src = src.substring(tokensLength); + continue; + } + // url (gfm) if (!inLink && (token = this.tokenizer.url(src, mangle))) { src = src.substring(token.raw.length); @@ -497,9 +602,14 @@ module.exports = class Lexer { continue; } + if (tokensLength = this.runTokenzierExtension(src, tokens, 'inlineText')) { + src = src.substring(tokensLength); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start cutSrc = src; - // find the next extension start, and clip 'src' up to that index if (this.options.extensions && this.options.extensions.startInline) { const match = src.match(this.options.extensions.startInline); if (match && match.length > 0) { diff --git a/src/marked.js b/src/marked.js index ccfb9d4583..f82697d9b7 100644 --- a/src/marked.js +++ b/src/marked.js @@ -151,67 +151,63 @@ marked.use = function(extension) { const extensions = {}; extension.forEach((ext) => { - if (ext.overwrite) { // Handle "overwrite" extensions - if (ext.renderer) { - const renderer = marked.defaults.renderer || new Renderer(); - for (const prop in ext.renderer) { - const prevRenderer = renderer[prop]; - renderer[prop] = (...args) => { - let ret = ext.renderer[prop].apply(renderer, args); - if (ret === false) { - ret = prevRenderer.apply(renderer, args); - } - return ret; - }; - } - opts.renderer = renderer; + //= =-- Parse "addon" extensions --==// + if (ext.renderer && ext.name) { // Renderers must have 'name' property + extensions[ext.name] = ext.renderer; + } + if (ext.tokenizer && ext.before) { // Tokenizers must have 'before' property + if (extensions[ext.before]) { + extensions[ext.before].push(ext.tokenizer); + } else { + extensions[ext.before] = [ext.tokenizer]; } - if (ext.tokenizer) { - const tokenizer = marked.defaults.tokenizer || new Tokenizer(); - for (const prop in ext.tokenizer) { - const prevTokenizer = tokenizer[prop]; - tokenizer[prop] = (...args) => { - let ret = ext.tokenizer[prop].apply(tokenizer, args); - if (ret === false) { - ret = prevTokenizer.apply(tokenizer, args); - } - return ret; - }; + if (ext.start && ext.level) { // Start regex must have 'level' property + if (ext.level === 'block') { + extensions.startBlock = extensions.startBlock + ? new RegExp(extensions.startBlock.source + '|' + ext.start.source) + : ext.start; + } else if (ext.level === 'inline') { + extensions.startInline = extensions.startInline + ? new RegExp(extensions.startInline.source + '|' + ext.start.source) + : ext.start; } - opts.tokenizer = tokenizer; } - } else { // Handle "addon" extensions - if (ext.renderer && ext.name) { - extensions[ext.name] = ext.renderer; - } - if (ext.tokenizer) { - if (ext.start && ext.level) { - if (ext.level === 'block') { - extensions.startBlock = extensions.startBlock - ? new RegExp(extensions.startBlock.source + '|' + ext.start.source) - : ext.start; - } else if (ext.level === 'inline') { - extensions.startInline = extensions.startInline - ? new RegExp(extensions.startInline.source + '|' + ext.start.source) - : ext.start; - } - } - if (ext.before) { - if (extensions[ext.before]) { - extensions[ext.before].push(ext.tokenizer); - } else { - extensions[ext.before] = [ext.tokenizer]; + } + if (Object.keys(extensions).length) { + opts.extensions = extensions; + } + + //= =-- Parse "overwrite" extensions --==// + if (ext.renderer && !ext.name) { + const renderer = marked.defaults.renderer || new Renderer(); + for (const prop in ext.renderer) { + const prevRenderer = renderer[prop]; + renderer[prop] = (...args) => { + let ret = ext.renderer[prop].apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); } - } else { // Handle extensions with no "before" set, will run after all others - if (extensions.last) { - extensions.last.push(ext.tokenizer); - } else { - extensions.last = [ext.tokenizer]; + return ret; + }; + } + opts.renderer = renderer; + } + if (ext.tokenizer && !ext.before) { + const tokenizer = marked.defaults.tokenizer || new Tokenizer(); + for (const prop in ext.tokenizer) { + const prevTokenizer = tokenizer[prop]; + tokenizer[prop] = (...args) => { + let ret = ext.tokenizer[prop].apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); } - } + return ret; + }; } + opts.tokenizer = tokenizer; } + //= =-- Parse WalkTokens extensions --==// if (ext.walkTokens) { const walkTokens = marked.defaults.walkTokens; opts.walkTokens = (token) => { @@ -223,9 +219,6 @@ marked.use = function(extension) { } }); - if (Object.keys(extensions).length) { - opts.extensions = extensions; - } marked.setOptions(opts); }; diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index ec25f711ef..f5d8c42540 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -219,7 +219,6 @@ describe('use extension', () => { it('should use renderer', () => { const extension = { - overwrite: true, renderer: { paragraph(text) { return 'extension'; @@ -235,7 +234,6 @@ describe('use extension', () => { it('should use tokenizer', () => { const extension = { - overwrite: true, tokenizer: { paragraph(text) { return { @@ -313,7 +311,6 @@ describe('use extension', () => { it('should use last extension function and not override others', () => { const extension1 = { - overwrite: true, renderer: { paragraph(text) { return 'extension1 paragraph\n'; @@ -324,7 +321,6 @@ describe('use extension', () => { } }; const extension2 = { - overwrite: true, renderer: { paragraph(text) { return 'extension2 paragraph\n'; @@ -345,7 +341,6 @@ paragraph it('should use previous extension when returning false', () => { const extension1 = { - overwrite: true, renderer: { paragraph(text) { if (text !== 'original') { @@ -356,7 +351,6 @@ paragraph } }; const extension2 = { - overwrite: true, renderer: { paragraph(text) { if (text !== 'extension1' && text !== 'original') { @@ -380,7 +374,6 @@ original it('should get options with this.options', () => { const extension = { - overwrite: true, renderer: { heading: () => { return this.options ? 'arrow options\n' : 'arrow no options\n'; From e7da3a009217c55fd0130ff1088e7474e0d413a9 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 11 May 2021 16:51:48 -0400 Subject: [PATCH 09/48] Typo --- src/Lexer.js | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index e7efe9a76f..ff62a808b5 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -102,7 +102,7 @@ module.exports = class Lexer { return lexer.inlineTokens(src); } - runTokenzierExtension(src, tokens, before) { + runTokenizerExtension(src, tokens, before) { let tokensLength = 0; if (this.options.extensions && this.options.extensions[before]) { let token; @@ -142,7 +142,7 @@ module.exports = class Lexer { let token, i, l, lastToken, tokensLength, cutSrc; while (src) { - if (this.runTokenzierExtension(src, tokens, 'space')) { + if (this.runTokenizerExtension(src, tokens, 'space')) { src = src.substring(tokensLength); continue; } @@ -156,7 +156,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'code')) { + if (this.runTokenizerExtension(src, tokens, 'code')) { src = src.substring(tokensLength); continue; } @@ -175,7 +175,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'fences')) { + if (this.runTokenizerExtension(src, tokens, 'fences')) { src = src.substring(tokensLength); continue; } @@ -187,7 +187,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'heading')) { + if (this.runTokenizerExtension(src, tokens, 'heading')) { src = src.substring(tokensLength); continue; } @@ -199,7 +199,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'nptable')) { + if (this.runTokenizerExtension(src, tokens, 'nptable')) { src = src.substring(tokensLength); continue; } @@ -211,7 +211,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'hr')) { + if (this.runTokenizerExtension(src, tokens, 'hr')) { src = src.substring(tokensLength); continue; } @@ -223,7 +223,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'blockquote')) { + if (this.runTokenizerExtension(src, tokens, 'blockquote')) { src = src.substring(tokensLength); continue; } @@ -236,7 +236,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'list')) { + if (this.runTokenizerExtension(src, tokens, 'list')) { src = src.substring(tokensLength); continue; } @@ -252,7 +252,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'html')) { + if (this.runTokenizerExtension(src, tokens, 'html')) { src = src.substring(tokensLength); continue; } @@ -264,7 +264,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'def')) { + if (this.runTokenizerExtension(src, tokens, 'def')) { src = src.substring(tokensLength); continue; } @@ -281,7 +281,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'table')) { + if (this.runTokenizerExtension(src, tokens, 'table')) { src = src.substring(tokensLength); continue; } @@ -293,7 +293,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'lheading')) { + if (this.runTokenizerExtension(src, tokens, 'lheading')) { src = src.substring(tokensLength); continue; } @@ -305,7 +305,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'paragraph')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'paragraph')) { src = src.substring(tokensLength); continue; } @@ -325,7 +325,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenzierExtension(src, tokens, 'text')) { + if (this.runTokenizerExtension(src, tokens, 'text')) { src = src.substring(tokensLength); continue; } @@ -460,7 +460,7 @@ module.exports = class Lexer { } keepPrevChar = false; - if (tokensLength = this.runTokenzierExtension(src, tokens, 'escape')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'escape')) { src = src.substring(tokensLength); continue; } @@ -472,7 +472,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'tag')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'tag')) { src = src.substring(tokensLength); continue; } @@ -492,7 +492,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'link')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'link')) { src = src.substring(tokensLength); continue; } @@ -507,7 +507,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'reflink')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'reflink')) { src = src.substring(tokensLength); continue; } @@ -528,7 +528,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'emStrong')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'emStrong')) { src = src.substring(tokensLength); continue; } @@ -541,7 +541,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'codespan')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'codespan')) { src = src.substring(tokensLength); continue; } @@ -553,7 +553,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'br')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'br')) { src = src.substring(tokensLength); continue; } @@ -565,7 +565,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'del')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'del')) { src = src.substring(tokensLength); continue; } @@ -578,7 +578,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'autolink')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'autolink')) { src = src.substring(tokensLength); continue; } @@ -590,7 +590,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'url')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'url')) { src = src.substring(tokensLength); continue; } @@ -602,7 +602,7 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenzierExtension(src, tokens, 'inlineText')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'inlineText')) { src = src.substring(tokensLength); continue; } From 87ade7eff53138b7637c161dd80d7ce0c066a173 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 11 May 2021 17:05:25 -0400 Subject: [PATCH 10/48] Another Typo --- src/Lexer.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index ff62a808b5..4120036d6c 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -142,7 +142,7 @@ module.exports = class Lexer { let token, i, l, lastToken, tokensLength, cutSrc; while (src) { - if (this.runTokenizerExtension(src, tokens, 'space')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'space')) { src = src.substring(tokensLength); continue; } @@ -156,7 +156,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'code')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'code')) { src = src.substring(tokensLength); continue; } @@ -175,7 +175,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'fences')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'fences')) { src = src.substring(tokensLength); continue; } @@ -187,7 +187,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'heading')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'heading')) { src = src.substring(tokensLength); continue; } @@ -199,7 +199,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'nptable')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'nptable')) { src = src.substring(tokensLength); continue; } @@ -211,7 +211,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'hr')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'hr')) { src = src.substring(tokensLength); continue; } @@ -223,7 +223,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'blockquote')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'blockquote')) { src = src.substring(tokensLength); continue; } @@ -236,7 +236,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'list')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'list')) { src = src.substring(tokensLength); continue; } @@ -252,7 +252,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'html')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'html')) { src = src.substring(tokensLength); continue; } @@ -264,7 +264,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'def')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'def')) { src = src.substring(tokensLength); continue; } @@ -281,7 +281,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'table')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'table')) { src = src.substring(tokensLength); continue; } @@ -293,7 +293,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'lheading')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'lheading')) { src = src.substring(tokensLength); continue; } @@ -325,7 +325,7 @@ module.exports = class Lexer { continue; } - if (this.runTokenizerExtension(src, tokens, 'text')) { + if (tokensLength = this.runTokenizerExtension(src, tokens, 'text')) { src = src.substring(tokensLength); continue; } From 244e07fdd6281a837464462d1f67ff1669a4efd3 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 12 May 2021 11:38:23 -0400 Subject: [PATCH 11/48] Run all Tokenizer extensions at top of Lexer loop / remove "before" --- lib/marked.esm.js | 573 +++++++++++++++++++++++---------------- src/Lexer.js | 171 ++---------- src/Parser.js | 5 +- src/marked.js | 19 +- test/unit/marked-spec.js | 3 - 5 files changed, 376 insertions(+), 395 deletions(-) diff --git a/lib/marked.esm.js b/lib/marked.esm.js index 71025919c9..5605cb61cb 100644 --- a/lib/marked.esm.js +++ b/lib/marked.esm.js @@ -11,10 +11,12 @@ var defaults$5 = {exports: {}}; -function getDefaults$1() { +var defaults = createCommonjsModule(function (module) { +function getDefaults() { return { baseUrl: null, breaks: false, + extensions: null, gfm: true, headerIds: true, headerPrefix: '', @@ -60,7 +62,7 @@ const escapeReplacements = { "'": ''' }; const getEscapeReplacement = (ch) => escapeReplacements[ch]; -function escape$3(html, encode) { +function escape(html, encode) { if (encode) { if (escapeTest.test(html)) { return html.replace(escapeReplace, getEscapeReplacement); @@ -76,7 +78,7 @@ function escape$3(html, encode) { const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; -function unescape$1(html) { +function unescape(html) { // explicitly match decimal, hex, and named HTML entities return html.replace(unescapeTest, (_, n) => { n = n.toLowerCase(); @@ -91,7 +93,7 @@ function unescape$1(html) { } const caret = /(^|[^\[])\^/g; -function edit$1(regex, opt) { +function edit(regex, opt) { regex = regex.source || regex; opt = opt || ''; const obj = { @@ -110,11 +112,11 @@ function edit$1(regex, opt) { const nonWordAndColonTest = /[^\w:]/g; const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; -function cleanUrl$1(sanitize, base, href) { +function cleanUrl(sanitize, base, href) { if (sanitize) { let prot; try { - prot = decodeURIComponent(unescape$1(href)) + prot = decodeURIComponent(unescape(href)) .replace(nonWordAndColonTest, '') .toLowerCase(); } catch (e) { @@ -148,7 +150,7 @@ function resolveUrl(base, href) { if (justDomain.test(base)) { baseUrls[' ' + base] = base + '/'; } else { - baseUrls[' ' + base] = rtrim$1(base, '/', true); + baseUrls[' ' + base] = rtrim(base, '/', true); } } base = baseUrls[' ' + base]; @@ -169,9 +171,9 @@ function resolveUrl(base, href) { } } -const noopTest$1 = { exec: function noopTest() {} }; +const noopTest = { exec: function noopTest() {} }; -function merge$2(obj) { +function merge(obj) { let i = 1, target, key; @@ -188,7 +190,7 @@ function merge$2(obj) { return obj; } -function splitCells$1(tableRow, count) { +function splitCells(tableRow, count) { // ensure that every cell-delimiting pipe has a space // before it to distinguish it from an escaped pipe const row = tableRow.replace(/\|/g, (match, offset, str) => { @@ -223,7 +225,7 @@ function splitCells$1(tableRow, count) { // Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). // /c*$/ is vulnerable to REDOS. // invert: Remove suffix of non-c chars instead. Default falsey. -function rtrim$1(str, c, invert) { +function rtrim(str, c, invert) { const l = str.length; if (l === 0) { return ''; @@ -247,7 +249,7 @@ function rtrim$1(str, c, invert) { return str.substr(0, l - suffLen); } -function findClosingBracket$1(str, b) { +function findClosingBracket(str, b) { if (str.indexOf(b[1]) === -1) { return -1; } @@ -269,14 +271,14 @@ function findClosingBracket$1(str, b) { return -1; } -function checkSanitizeDeprecation$1(opt) { +function checkSanitizeDeprecation(opt) { if (opt && opt.sanitize && !opt.silent) { console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options'); } } // copied from https://stackoverflow.com/a/5450113/806777 -function repeatString$1(pattern, count) { +function repeatString(pattern, count) { if (count < 1) { return ''; } @@ -292,31 +294,31 @@ function repeatString$1(pattern, count) { } var helpers = { - escape: escape$3, - unescape: unescape$1, - edit: edit$1, - cleanUrl: cleanUrl$1, + escape, + unescape, + edit, + cleanUrl, resolveUrl, - noopTest: noopTest$1, - merge: merge$2, - splitCells: splitCells$1, - rtrim: rtrim$1, - findClosingBracket: findClosingBracket$1, - checkSanitizeDeprecation: checkSanitizeDeprecation$1, - repeatString: repeatString$1 + noopTest, + merge, + splitCells, + rtrim, + findClosingBracket, + checkSanitizeDeprecation, + repeatString }; -const { defaults: defaults$4 } = defaults$5.exports; +const { defaults: defaults$1 } = defaults; const { - rtrim, - splitCells, - escape: escape$2, - findClosingBracket + rtrim: rtrim$1, + splitCells: splitCells$1, + escape: escape$1, + findClosingBracket: findClosingBracket$1 } = helpers; function outputLink(cap, link, raw) { const href = link.href; - const title = link.title ? escape$2(link.title) : null; + const title = link.title ? escape$1(link.title) : null; const text = cap[1].replace(/\\([\[\]])/g, '$1'); if (cap[0].charAt(0) !== '!') { @@ -333,7 +335,7 @@ function outputLink(cap, link, raw) { raw, href, title, - text: escape$2(text) + text: escape$1(text) }; } } @@ -371,7 +373,7 @@ function indentCodeCompensation(raw, text) { */ var Tokenizer_1 = class Tokenizer { constructor(options) { - this.options = options || defaults$4; + this.options = options || defaults$1; } space(src) { @@ -396,7 +398,7 @@ var Tokenizer_1 = class Tokenizer { raw: cap[0], codeBlockStyle: 'indented', text: !this.options.pedantic - ? rtrim(text, '\n') + ? rtrim$1(text, '\n') : text }; } @@ -424,7 +426,7 @@ var Tokenizer_1 = class Tokenizer { // remove trailing #s if (/#$/.test(text)) { - const trimmed = rtrim(text, '#'); + const trimmed = rtrim$1(text, '#'); if (this.options.pedantic) { text = trimmed.trim(); } else if (!trimmed || / $/.test(trimmed)) { @@ -447,7 +449,7 @@ var Tokenizer_1 = class Tokenizer { if (cap) { const item = { type: 'table', - header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), + header: splitCells$1(cap[1].replace(/^ *| *\| *$/g, '')), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [], raw: cap[0] @@ -470,7 +472,7 @@ var Tokenizer_1 = class Tokenizer { l = item.cells.length; for (i = 0; i < l; i++) { - item.cells[i] = splitCells(item.cells[i], item.header.length); + item.cells[i] = splitCells$1(item.cells[i], item.header.length); } return item; @@ -592,7 +594,7 @@ var Tokenizer_1 = class Tokenizer { } // trim item newlines at end - item = rtrim(item, '\n'); + item = rtrim$1(item, '\n'); if (i !== l - 1) { raw = raw + '\n'; } @@ -644,7 +646,7 @@ var Tokenizer_1 = class Tokenizer { raw: cap[0], pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), - text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape$2(cap[0])) : cap[0] + text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape$1(cap[0])) : cap[0] }; } } @@ -669,7 +671,7 @@ var Tokenizer_1 = class Tokenizer { if (cap) { const item = { type: 'table', - header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), + header: splitCells$1(cap[1].replace(/^ *| *\| *$/g, '')), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] }; @@ -693,7 +695,7 @@ var Tokenizer_1 = class Tokenizer { l = item.cells.length; for (i = 0; i < l; i++) { - item.cells[i] = splitCells( + item.cells[i] = splitCells$1( item.cells[i].replace(/^ *\| *| *\| *$/g, ''), item.header.length); } @@ -745,7 +747,7 @@ var Tokenizer_1 = class Tokenizer { return { type: 'escape', raw: cap[0], - text: escape$2(cap[1]) + text: escape$1(cap[1]) }; } } @@ -774,7 +776,7 @@ var Tokenizer_1 = class Tokenizer { text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) - : escape$2(cap[0])) + : escape$1(cap[0])) : cap[0] }; } @@ -791,13 +793,13 @@ var Tokenizer_1 = class Tokenizer { } // ending angle bracket cannot be escaped - const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + const rtrimSlash = rtrim$1(trimmedUrl.slice(0, -1), '\\'); if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { return; } } else { // find closing parenthesis - const lastParenIndex = findClosingBracket(cap[2], '()'); + const lastParenIndex = findClosingBracket$1(cap[2], '()'); if (lastParenIndex > -1) { const start = cap[0].indexOf('!') === 0 ? 5 : 4; const linkLen = start + cap[1].length + lastParenIndex; @@ -924,7 +926,7 @@ var Tokenizer_1 = class Tokenizer { if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { text = text.substring(1, text.length - 1); } - text = escape$2(text, true); + text = escape$1(text, true); return { type: 'codespan', raw: cap[0], @@ -959,10 +961,10 @@ var Tokenizer_1 = class Tokenizer { if (cap) { let text, href; if (cap[2] === '@') { - text = escape$2(this.options.mangle ? mangle(cap[1]) : cap[1]); + text = escape$1(this.options.mangle ? mangle(cap[1]) : cap[1]); href = 'mailto:' + text; } else { - text = escape$2(cap[1]); + text = escape$1(cap[1]); href = text; } @@ -987,7 +989,7 @@ var Tokenizer_1 = class Tokenizer { if (cap = this.rules.inline.url.exec(src)) { let text, href; if (cap[2] === '@') { - text = escape$2(this.options.mangle ? mangle(cap[0]) : cap[0]); + text = escape$1(this.options.mangle ? mangle(cap[0]) : cap[0]); href = 'mailto:' + text; } else { // do extended autolink path validation @@ -996,7 +998,7 @@ var Tokenizer_1 = class Tokenizer { prevCapZero = cap[0]; cap[0] = this.rules.inline._backpedal.exec(cap[0])[0]; } while (prevCapZero !== cap[0]); - text = escape$2(cap[0]); + text = escape$1(cap[0]); if (cap[1] === 'www.') { href = 'http://' + text; } else { @@ -1024,9 +1026,9 @@ var Tokenizer_1 = class Tokenizer { if (cap) { let text; if (inRawBlock) { - text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape$2(cap[0])) : cap[0]; + text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape$1(cap[0])) : cap[0]; } else { - text = escape$2(this.options.smartypants ? smartypants(cap[0]) : cap[0]); + text = escape$1(this.options.smartypants ? smartypants(cap[0]) : cap[0]); } return { type: 'text', @@ -1038,15 +1040,15 @@ var Tokenizer_1 = class Tokenizer { }; const { - noopTest, - edit, + noopTest: noopTest$1, + edit: edit$1, merge: merge$1 } = helpers; /** * Block-Level Grammar */ -const block$1 = { +const block = { newline: /^(?: *(?:\n|$))+/, code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/, fences: /^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/, @@ -1065,8 +1067,8 @@ const block$1 = { + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag + ')', def: /^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/, - nptable: noopTest, - table: noopTest, + nptable: noopTest$1, + table: noopTest$1, lheading: /^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/, // regex template, placeholders will be replaced according to different paragraph // interruption rules of commonmark and the original markdown spec: @@ -1074,68 +1076,68 @@ const block$1 = { text: /^[^\n]+/ }; -block$1._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/; -block$1._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; -block$1.def = edit(block$1.def) - .replace('label', block$1._label) - .replace('title', block$1._title) +block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/; +block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; +block.def = edit$1(block.def) + .replace('label', block._label) + .replace('title', block._title) .getRegex(); -block$1.bullet = /(?:[*+-]|\d{1,9}[.)])/; -block$1.item = /^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/; -block$1.item = edit(block$1.item, 'gm') - .replace(/bull/g, block$1.bullet) +block.bullet = /(?:[*+-]|\d{1,9}[.)])/; +block.item = /^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/; +block.item = edit$1(block.item, 'gm') + .replace(/bull/g, block.bullet) .getRegex(); -block$1.listItemStart = edit(/^( *)(bull) */) - .replace('bull', block$1.bullet) +block.listItemStart = edit$1(/^( *)(bull) */) + .replace('bull', block.bullet) .getRegex(); -block$1.list = edit(block$1.list) - .replace(/bull/g, block$1.bullet) +block.list = edit$1(block.list) + .replace(/bull/g, block.bullet) .replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))') - .replace('def', '\\n+(?=' + block$1.def.source + ')') + .replace('def', '\\n+(?=' + block.def.source + ')') .getRegex(); -block$1._tag = 'address|article|aside|base|basefont|blockquote|body|caption' +block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' + '|track|ul'; -block$1._comment = /|$)/; -block$1.html = edit(block$1.html, 'i') - .replace('comment', block$1._comment) - .replace('tag', block$1._tag) +block._comment = /|$)/; +block.html = edit$1(block.html, 'i') + .replace('comment', block._comment) + .replace('tag', block._tag) .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) .getRegex(); -block$1.paragraph = edit(block$1._paragraph) - .replace('hr', block$1.hr) +block.paragraph = edit$1(block._paragraph) + .replace('hr', block.hr) .replace('heading', ' {0,3}#{1,6} ') .replace('|lheading', '') // setex headings don't interrupt commonmark paragraphs .replace('blockquote', ' {0,3}>') .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt .replace('html', ')|<(?:script|pre|style|!--)') - .replace('tag', block$1._tag) // pars can be interrupted by type (6) html blocks + .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks .getRegex(); -block$1.blockquote = edit(block$1.blockquote) - .replace('paragraph', block$1.paragraph) +block.blockquote = edit$1(block.blockquote) + .replace('paragraph', block.paragraph) .getRegex(); /** * Normal Block Grammar */ -block$1.normal = merge$1({}, block$1); +block.normal = merge$1({}, block); /** * GFM Block Grammar */ -block$1.gfm = merge$1({}, block$1.normal, { +block.gfm = merge$1({}, block.normal, { nptable: '^ *([^|\\n ].*\\|.*)\\n' // Header + ' {0,3}([-:]+ *\\|[-| :]*)' // Align + '(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)', // Cells @@ -1144,38 +1146,38 @@ block$1.gfm = merge$1({}, block$1.normal, { + '(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)' // Cells }); -block$1.gfm.nptable = edit(block$1.gfm.nptable) - .replace('hr', block$1.hr) +block.gfm.nptable = edit$1(block.gfm.nptable) + .replace('hr', block.hr) .replace('heading', ' {0,3}#{1,6} ') .replace('blockquote', ' {0,3}>') .replace('code', ' {4}[^\\n]') .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt .replace('html', ')|<(?:script|pre|style|!--)') - .replace('tag', block$1._tag) // tables can be interrupted by type (6) html blocks + .replace('tag', block._tag) // tables can be interrupted by type (6) html blocks .getRegex(); -block$1.gfm.table = edit(block$1.gfm.table) - .replace('hr', block$1.hr) +block.gfm.table = edit$1(block.gfm.table) + .replace('hr', block.hr) .replace('heading', ' {0,3}#{1,6} ') .replace('blockquote', ' {0,3}>') .replace('code', ' {4}[^\\n]') .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt .replace('html', ')|<(?:script|pre|style|!--)') - .replace('tag', block$1._tag) // tables can be interrupted by type (6) html blocks + .replace('tag', block._tag) // tables can be interrupted by type (6) html blocks .getRegex(); /** * Pedantic grammar (original John Gruber's loose markdown specification) */ -block$1.pedantic = merge$1({}, block$1.normal, { - html: edit( +block.pedantic = merge$1({}, block.normal, { + html: edit$1( '^ *(?:comment *(?:\\n|\\s*$)' + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') - .replace('comment', block$1._comment) + .replace('comment', block._comment) .replace(/tag/g, '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' @@ -1183,11 +1185,11 @@ block$1.pedantic = merge$1({}, block$1.normal, { .getRegex(), def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, heading: /^(#{1,6})(.*)(?:\n+|$)/, - fences: noopTest, // fences not supported - paragraph: edit(block$1.normal._paragraph) - .replace('hr', block$1.hr) + fences: noopTest$1, // fences not supported + paragraph: edit$1(block.normal._paragraph) + .replace('hr', block.hr) .replace('heading', ' *#{1,6} *[^\n]') - .replace('lheading', block$1.lheading) + .replace('lheading', block.lheading) .replace('blockquote', ' {0,3}>') .replace('|fences', '') .replace('|list', '') @@ -1198,10 +1200,10 @@ block$1.pedantic = merge$1({}, block$1.normal, { /** * Inline-Level Grammar */ -const inline$1 = { +const inline = { escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, - url: noopTest, + url: noopTest$1, tag: '^comment' + '|^' // self-closing tag + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag @@ -1221,80 +1223,80 @@ const inline$1 = { }, code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, br: /^( {2,}|\\)\n(?!\s*$)/, - del: noopTest, + del: noopTest$1, text: /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~'; -inline$1.punctuation = edit(inline$1.punctuation).replace(/punctuation/g, inline$1._punctuation).getRegex(); +inline._punctuation = '!"#$%&\'()+\\-.,/:;<=>?@\\[\\]`^{|}~'; +inline.punctuation = edit$1(inline.punctuation).replace(/punctuation/g, inline._punctuation).getRegex(); // sequences em should skip over [title](link), `code`, -inline$1.blockSkip = /\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g; -inline$1.escapedEmSt = /\\\*|\\_/g; +inline.blockSkip = /\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g; +inline.escapedEmSt = /\\\*|\\_/g; -inline$1._comment = edit(block$1._comment).replace('(?:-->|$)', '-->').getRegex(); +inline._comment = edit$1(block._comment).replace('(?:-->|$)', '-->').getRegex(); -inline$1.emStrong.lDelim = edit(inline$1.emStrong.lDelim) - .replace(/punct/g, inline$1._punctuation) +inline.emStrong.lDelim = edit$1(inline.emStrong.lDelim) + .replace(/punct/g, inline._punctuation) .getRegex(); -inline$1.emStrong.rDelimAst = edit(inline$1.emStrong.rDelimAst, 'g') - .replace(/punct/g, inline$1._punctuation) +inline.emStrong.rDelimAst = edit$1(inline.emStrong.rDelimAst, 'g') + .replace(/punct/g, inline._punctuation) .getRegex(); -inline$1.emStrong.rDelimUnd = edit(inline$1.emStrong.rDelimUnd, 'g') - .replace(/punct/g, inline$1._punctuation) +inline.emStrong.rDelimUnd = edit$1(inline.emStrong.rDelimUnd, 'g') + .replace(/punct/g, inline._punctuation) .getRegex(); -inline$1._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g; +inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g; -inline$1._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; -inline$1._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; -inline$1.autolink = edit(inline$1.autolink) - .replace('scheme', inline$1._scheme) - .replace('email', inline$1._email) +inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; +inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; +inline.autolink = edit$1(inline.autolink) + .replace('scheme', inline._scheme) + .replace('email', inline._email) .getRegex(); -inline$1._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; +inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; -inline$1.tag = edit(inline$1.tag) - .replace('comment', inline$1._comment) - .replace('attribute', inline$1._attribute) +inline.tag = edit$1(inline.tag) + .replace('comment', inline._comment) + .replace('attribute', inline._attribute) .getRegex(); -inline$1._label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; -inline$1._href = /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/; -inline$1._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; +inline._label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; +inline._href = /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/; +inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; -inline$1.link = edit(inline$1.link) - .replace('label', inline$1._label) - .replace('href', inline$1._href) - .replace('title', inline$1._title) +inline.link = edit$1(inline.link) + .replace('label', inline._label) + .replace('href', inline._href) + .replace('title', inline._title) .getRegex(); -inline$1.reflink = edit(inline$1.reflink) - .replace('label', inline$1._label) +inline.reflink = edit$1(inline.reflink) + .replace('label', inline._label) .getRegex(); -inline$1.reflinkSearch = edit(inline$1.reflinkSearch, 'g') - .replace('reflink', inline$1.reflink) - .replace('nolink', inline$1.nolink) +inline.reflinkSearch = edit$1(inline.reflinkSearch, 'g') + .replace('reflink', inline.reflink) + .replace('nolink', inline.nolink) .getRegex(); /** * Normal Inline Grammar */ -inline$1.normal = merge$1({}, inline$1); +inline.normal = merge$1({}, inline); /** * Pedantic Inline Grammar */ -inline$1.pedantic = merge$1({}, inline$1.normal, { +inline.pedantic = merge$1({}, inline.normal, { strong: { start: /^__|\*\*/, middle: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, @@ -1307,11 +1309,11 @@ inline$1.pedantic = merge$1({}, inline$1.normal, { endAst: /\*(?!\*)/g, endUnd: /_(?!_)/g }, - link: edit(/^!?\[(label)\]\((.*?)\)/) - .replace('label', inline$1._label) + link: edit$1(/^!?\[(label)\]\((.*?)\)/) + .replace('label', inline._label) .getRegex(), - reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) - .replace('label', inline$1._label) + reflink: edit$1(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', inline._label) .getRegex() }); @@ -1319,8 +1321,8 @@ inline$1.pedantic = merge$1({}, inline$1.normal, { * GFM Inline Grammar */ -inline$1.gfm = merge$1({}, inline$1.normal, { - escape: edit(inline$1.escape).replace('])', '~|])').getRegex(), +inline.gfm = merge$1({}, inline.normal, { + escape: edit$1(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]+;$)|[?!.,:;*_~)]+(?!$))+/, @@ -1328,30 +1330,29 @@ inline$1.gfm = merge$1({}, inline$1.normal, { text: /^([`~]+|[^`~])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ 0) { + cutSrc = src.substring(0, match.index); + } + } + if (top && (token = this.tokenizer.paragraph(cutSrc))) { src = src.substring(token.raw.length); tokens.push(token); continue; @@ -1679,12 +1701,12 @@ var Lexer_1 = class Lexer { * Lexing/Compiling */ inlineTokens(src, tokens = [], inLink = false, inRawBlock = false) { - let token, lastToken; + let token, lastToken, cutSrc; // String with links masked to avoid interference with em and strong let maskedSrc = src; let match; - let keepPrevChar, prevChar; + let keepPrevChar, prevChar, tokensParsed; // Mask out reflinks if (this.tokens.links) { @@ -1692,14 +1714,14 @@ var Lexer_1 = class Lexer { if (links.length > 0) { while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { - maskedSrc = maskedSrc.slice(0, match.index) + '[' + repeatString('a', match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + maskedSrc = maskedSrc.slice(0, match.index) + '[' + repeatString$1('a', match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); } } } } // Mask out other blocks while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { - maskedSrc = maskedSrc.slice(0, match.index) + '[' + repeatString('a', match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + maskedSrc = maskedSrc.slice(0, match.index) + '[' + repeatString$1('a', match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); } // Mask out escaped em & strong delimiters @@ -1713,6 +1735,19 @@ var Lexer_1 = class Lexer { } keepPrevChar = false; + // extensions + if (this.options.extensions?.inline) { + tokensParsed = false; + this.options.extensions.inline.forEach(function(extTokenizer, index) { + if (token = extTokenizer(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + tokensParsed = true; + } + }); + if (tokensParsed) { continue; } + } + // escape if (token = this.tokenizer.escape(src)) { src = src.substring(token.raw.length); @@ -1725,7 +1760,7 @@ var Lexer_1 = class Lexer { src = src.substring(token.raw.length); inLink = token.inLink; inRawBlock = token.inRawBlock; - const lastToken = tokens[tokens.length - 1]; + lastToken = tokens[tokens.length - 1]; if (lastToken && token.type === 'text' && lastToken.type === 'text') { lastToken.raw += token.raw; lastToken.text += token.text; @@ -1748,7 +1783,7 @@ var Lexer_1 = class Lexer { // reflink, nolink if (token = this.tokenizer.reflink(src, this.tokens.links)) { src = src.substring(token.raw.length); - const lastToken = tokens[tokens.length - 1]; + lastToken = tokens[tokens.length - 1]; if (token.type === 'link') { token.tokens = this.inlineTokens(token.text, [], true, inRawBlock); tokens.push(token); @@ -1806,7 +1841,15 @@ var Lexer_1 = class Lexer { } // text - if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) { + // prevent inlineText consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions?.startInline) { + const match = src.match(this.options.extensions.startInline); + if (match && match.length > 0) { + cutSrc = src.substring(0, match.index); + } + } + if (token = this.tokenizer.inlineText(cutSrc, inRawBlock, smartypants)) { src = src.substring(token.raw.length); if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started prevChar = token.raw.slice(-1); @@ -1837,10 +1880,10 @@ var Lexer_1 = class Lexer { } }; -const { defaults: defaults$2 } = defaults$5.exports; +const { defaults: defaults$3 } = defaults; const { - cleanUrl, - escape: escape$1 + cleanUrl: cleanUrl$1, + escape: escape$2 } = helpers; /** @@ -1848,7 +1891,7 @@ const { */ var Renderer_1 = class Renderer { constructor(options) { - this.options = options || defaults$2; + this.options = options || defaults$3; } code(code, infostring, escaped) { @@ -1865,15 +1908,15 @@ var Renderer_1 = class Renderer { if (!lang) { return '
'
-        + (escaped ? code : escape$1(code, true))
+        + (escaped ? code : escape$2(code, true))
         + '
\n'; } return '
'
-      + (escaped ? code : escape$1(code, true))
+      + (escaped ? code : escape$2(code, true))
       + '
\n'; } @@ -1973,11 +2016,11 @@ var Renderer_1 = class Renderer { } link(href, title, text) { - href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + href = cleanUrl$1(this.options.sanitize, this.options.baseUrl, href); if (href === null) { return text; } - let out = 'An error occurred:

'
-        + escape(e.message + '', true)
+        + escape$3(e.message + '', true)
         + '
'; } throw e; @@ -2493,58 +2552,98 @@ function marked(src, opt, callback) { marked.options = marked.setOptions = function(opt) { - merge(marked.defaults, opt); + merge$2(marked.defaults, opt); changeDefaults(marked.defaults); return marked; }; marked.getDefaults = getDefaults; -marked.defaults = defaults; +marked.defaults = defaults$5; /** * Use Extension */ marked.use = function(extension) { - const opts = merge({}, extension); - if (extension.renderer) { - const renderer = marked.defaults.renderer || new Renderer(); - for (const prop in extension.renderer) { - const prevRenderer = renderer[prop]; - renderer[prop] = (...args) => { - let ret = extension.renderer[prop].apply(renderer, args); - if (ret === false) { - ret = prevRenderer.apply(renderer, args); - } - return ret; - }; + if (!Array.isArray(extension)) { // Wrap in array if not already to unify processing + extension = [extension]; + } + const opts = merge$2({}, ...extension); + opts.tokenizer = null; + opts.renderer = null; + const extensions = {}; + + extension.forEach((ext) => { + //= =-- Parse "addon" extensions --==// + if (ext.renderer && ext.name) { // Renderers must have 'name' property + extensions[ext.name] = ext.renderer; + } + if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property + if (extensions[ext.level]) { + extensions[ext.level].push(ext.tokenizer); + } else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Regex to check for start of token + if (ext.level === 'block') { + extensions.startBlock = extensions.startBlock + ? new RegExp(extensions.startBlock.source + '|' + ext.start.source) + : ext.start; + } else if (ext.level === 'inline') { + extensions.startInline = extensions.startInline + ? new RegExp(extensions.startInline.source + '|' + ext.start.source) + : ext.start; + } + } } - opts.renderer = renderer; - } - if (extension.tokenizer) { - const tokenizer = marked.defaults.tokenizer || new Tokenizer(); - for (const prop in extension.tokenizer) { - const prevTokenizer = tokenizer[prop]; - tokenizer[prop] = (...args) => { - let ret = extension.tokenizer[prop].apply(tokenizer, args); - if (ret === false) { - ret = prevTokenizer.apply(tokenizer, args); + + //= =-- Parse "overwrite" extensions --==// + if (ext.renderer && !ext.name) { + const renderer = marked.defaults.renderer || new Renderer_1(); + for (const prop in ext.renderer) { + const prevRenderer = renderer[prop]; + renderer[prop] = (...args) => { + let ret = ext.renderer[prop].apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret; + }; + } + opts.renderer = renderer; + } + if (ext.tokenizer && !ext.level) { + const tokenizer = marked.defaults.tokenizer || new Tokenizer_1(); + for (const prop in ext.tokenizer) { + const prevTokenizer = tokenizer[prop]; + tokenizer[prop] = (...args) => { + let ret = ext.tokenizer[prop].apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + + //= =-- Parse WalkTokens extensions --==// + if (ext.walkTokens) { + const walkTokens = marked.defaults.walkTokens; + opts.walkTokens = (token) => { + ext.walkTokens(token); + if (walkTokens) { + walkTokens(token); } - return ret; }; } - opts.tokenizer = tokenizer; - } - if (extension.walkTokens) { - const walkTokens = marked.defaults.walkTokens; - opts.walkTokens = (token) => { - extension.walkTokens(token); - if (walkTokens) { - walkTokens(token); - } - }; + }); + + if (Object.keys(extensions).length) { + opts.extensions = extensions; } + marked.setOptions(opts); }; @@ -2593,8 +2692,8 @@ marked.parseInline = function(src, opt) { + Object.prototype.toString.call(src) + ', string expected'); } - opt = merge({}, marked.defaults, opt || {}); - checkSanitizeDeprecation(opt); + opt = merge$2({}, marked.defaults, opt || {}); + checkSanitizeDeprecation$1(opt); try { const tokens = Lexer.lexInline(src, opt); @@ -2606,7 +2705,7 @@ marked.parseInline = function(src, opt) { e.message += '\nPlease report this to https://github.com/markedjs/marked.'; if (opt.silent) { return '

An error occurred:

'
-        + escape(e.message + '', true)
+        + escape$3(e.message + '', true)
         + '
'; } throw e; diff --git a/src/Lexer.js b/src/Lexer.js index 4120036d6c..320ae6b3f8 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -52,7 +52,6 @@ module.exports = class Lexer { this.tokens = []; this.tokens.links = Object.create(null); this.options = options || defaults; - this.options.extensions = this.options.extensions || null; this.options.tokenizer = this.options.tokenizer || new Tokenizer(); this.tokenizer = this.options.tokenizer; this.tokenizer.options = this.options; @@ -102,21 +101,6 @@ module.exports = class Lexer { return lexer.inlineTokens(src); } - runTokenizerExtension(src, tokens, before) { - let tokensLength = 0; - if (this.options.extensions && this.options.extensions[before]) { - let token; - this.options.extensions[before].forEach(function(extTokenizer, index) { - if (token = extTokenizer(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - tokensLength += token.raw.length; - } - }); - } - return tokensLength; - } - /** * Preprocessing */ @@ -139,12 +123,20 @@ module.exports = class Lexer { if (this.options.pedantic) { src = src.replace(/^ +$/gm, ''); } - let token, i, l, lastToken, tokensLength, cutSrc; + let token, i, l, lastToken, tokensParsed, cutSrc; while (src) { - if (tokensLength = this.runTokenizerExtension(src, tokens, 'space')) { - src = src.substring(tokensLength); - continue; + // extensions + if (this.options.extensions?.block) { + tokensParsed = false; + this.options.extensions.block.forEach(function(extTokenizer, index) { + if (token = extTokenizer(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + tokensParsed = true; + } + }); + if (tokensParsed) { continue; } } // newline @@ -156,11 +148,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'code')) { - src = src.substring(tokensLength); - continue; - } - // code if (token = this.tokenizer.code(src)) { src = src.substring(token.raw.length); @@ -175,11 +162,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'fences')) { - src = src.substring(tokensLength); - continue; - } - // fences if (token = this.tokenizer.fences(src)) { src = src.substring(token.raw.length); @@ -187,11 +169,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'heading')) { - src = src.substring(tokensLength); - continue; - } - // heading if (token = this.tokenizer.heading(src)) { src = src.substring(token.raw.length); @@ -199,11 +176,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'nptable')) { - src = src.substring(tokensLength); - continue; - } - // table no leading pipe (gfm) if (token = this.tokenizer.nptable(src)) { src = src.substring(token.raw.length); @@ -211,11 +183,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'hr')) { - src = src.substring(tokensLength); - continue; - } - // hr if (token = this.tokenizer.hr(src)) { src = src.substring(token.raw.length); @@ -223,11 +190,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'blockquote')) { - src = src.substring(tokensLength); - continue; - } - // blockquote if (token = this.tokenizer.blockquote(src)) { src = src.substring(token.raw.length); @@ -236,11 +198,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'list')) { - src = src.substring(tokensLength); - continue; - } - // list if (token = this.tokenizer.list(src)) { src = src.substring(token.raw.length); @@ -252,11 +209,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'html')) { - src = src.substring(tokensLength); - continue; - } - // html if (token = this.tokenizer.html(src)) { src = src.substring(token.raw.length); @@ -264,11 +216,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'def')) { - src = src.substring(tokensLength); - continue; - } - // def if (top && (token = this.tokenizer.def(src))) { src = src.substring(token.raw.length); @@ -281,11 +228,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'table')) { - src = src.substring(tokensLength); - continue; - } - // table (gfm) if (token = this.tokenizer.table(src)) { src = src.substring(token.raw.length); @@ -293,11 +235,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'lheading')) { - src = src.substring(tokensLength); - continue; - } - // lheading if (token = this.tokenizer.lheading(src)) { src = src.substring(token.raw.length); @@ -305,15 +242,10 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'paragraph')) { - src = src.substring(tokensLength); - continue; - } - // top-level paragraph // prevent paragraph consuming extensions by clipping 'src' to extension start cutSrc = src; - if (this.options.extensions && this.options.extensions.startBlock) { + if (this.options.extensions?.startBlock) { const match = src.match(this.options.extensions.startBlock); if (match && match.length > 0) { cutSrc = src.substring(0, match.index); @@ -325,11 +257,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'text')) { - src = src.substring(tokensLength); - continue; - } - // text if (token = this.tokenizer.text(src)) { src = src.substring(token.raw.length); @@ -431,7 +358,7 @@ module.exports = class Lexer { // String with links masked to avoid interference with em and strong let maskedSrc = src; let match; - let keepPrevChar, prevChar, tokensLength; + let keepPrevChar, prevChar, tokensParsed; // Mask out reflinks if (this.tokens.links) { @@ -460,9 +387,17 @@ module.exports = class Lexer { } keepPrevChar = false; - if (tokensLength = this.runTokenizerExtension(src, tokens, 'escape')) { - src = src.substring(tokensLength); - continue; + // extensions + if (this.options.extensions?.inline) { + tokensParsed = false; + this.options.extensions.inline.forEach(function(extTokenizer, index) { + if (token = extTokenizer(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + tokensParsed = true; + } + }); + if (tokensParsed) { continue; } } // escape @@ -472,17 +407,12 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'tag')) { - src = src.substring(tokensLength); - continue; - } - // tag if (token = this.tokenizer.tag(src, inLink, inRawBlock)) { src = src.substring(token.raw.length); inLink = token.inLink; inRawBlock = token.inRawBlock; - const lastToken = tokens[tokens.length - 1]; + lastToken = tokens[tokens.length - 1]; if (lastToken && token.type === 'text' && lastToken.type === 'text') { lastToken.raw += token.raw; lastToken.text += token.text; @@ -492,11 +422,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'link')) { - src = src.substring(tokensLength); - continue; - } - // link if (token = this.tokenizer.link(src)) { src = src.substring(token.raw.length); @@ -507,15 +432,10 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'reflink')) { - src = src.substring(tokensLength); - continue; - } - // reflink, nolink if (token = this.tokenizer.reflink(src, this.tokens.links)) { src = src.substring(token.raw.length); - const lastToken = tokens[tokens.length - 1]; + lastToken = tokens[tokens.length - 1]; if (token.type === 'link') { token.tokens = this.inlineTokens(token.text, [], true, inRawBlock); tokens.push(token); @@ -528,11 +448,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'emStrong')) { - src = src.substring(tokensLength); - continue; - } - // em & strong if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { src = src.substring(token.raw.length); @@ -541,11 +456,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'codespan')) { - src = src.substring(tokensLength); - continue; - } - // code if (token = this.tokenizer.codespan(src)) { src = src.substring(token.raw.length); @@ -553,11 +463,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'br')) { - src = src.substring(tokensLength); - continue; - } - // br if (token = this.tokenizer.br(src)) { src = src.substring(token.raw.length); @@ -565,11 +470,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'del')) { - src = src.substring(tokensLength); - continue; - } - // del (gfm) if (token = this.tokenizer.del(src)) { src = src.substring(token.raw.length); @@ -578,11 +478,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'autolink')) { - src = src.substring(tokensLength); - continue; - } - // autolink if (token = this.tokenizer.autolink(src, mangle)) { src = src.substring(token.raw.length); @@ -590,11 +485,6 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'url')) { - src = src.substring(tokensLength); - continue; - } - // url (gfm) if (!inLink && (token = this.tokenizer.url(src, mangle))) { src = src.substring(token.raw.length); @@ -602,15 +492,10 @@ module.exports = class Lexer { continue; } - if (tokensLength = this.runTokenizerExtension(src, tokens, 'inlineText')) { - src = src.substring(tokensLength); - continue; - } - // text // prevent inlineText consuming extensions by clipping 'src' to extension start cutSrc = src; - if (this.options.extensions && this.options.extensions.startInline) { + if (this.options.extensions?.startInline) { const match = src.match(this.options.extensions.startInline); if (match && match.length > 0) { cutSrc = src.substring(0, match.index); diff --git a/src/Parser.js b/src/Parser.js index 28b5ec6a17..733fc64d0f 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -12,7 +12,6 @@ const { module.exports = class Parser { constructor(options) { this.options = options || defaults; - this.options.extensions = this.options.extensions || null; this.options.renderer = this.options.renderer || new Renderer(); this.renderer = this.options.renderer; this.renderer.options = this.options; @@ -185,7 +184,7 @@ module.exports = class Parser { default: { // Run any renderer extensions tokenParsed = false; - if (this.options.extensions && this.options.extensions[token.type]) { + if (this.options.extensions?.[token.type]) { out += this.options.extensions[token.type](token); tokenParsed = true; } @@ -262,7 +261,7 @@ module.exports = class Parser { default: { // Run any renderer extensions tokenParsed = false; - if (this.options.extensions && this.options.extensions[token.type]) { + if (this.options.extensions?.[token.type]) { out += this.options.extensions[token.type](token); tokenParsed = true; } diff --git a/src/marked.js b/src/marked.js index f82697d9b7..fb59edb3c3 100644 --- a/src/marked.js +++ b/src/marked.js @@ -155,13 +155,13 @@ marked.use = function(extension) { if (ext.renderer && ext.name) { // Renderers must have 'name' property extensions[ext.name] = ext.renderer; } - if (ext.tokenizer && ext.before) { // Tokenizers must have 'before' property - if (extensions[ext.before]) { - extensions[ext.before].push(ext.tokenizer); + if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property + if (extensions[ext.level]) { + extensions[ext.level].push(ext.tokenizer); } else { - extensions[ext.before] = [ext.tokenizer]; + extensions[ext.level] = [ext.tokenizer]; } - if (ext.start && ext.level) { // Start regex must have 'level' property + if (ext.start) { // Regex to check for start of token if (ext.level === 'block') { extensions.startBlock = extensions.startBlock ? new RegExp(extensions.startBlock.source + '|' + ext.start.source) @@ -173,9 +173,6 @@ marked.use = function(extension) { } } } - if (Object.keys(extensions).length) { - opts.extensions = extensions; - } //= =-- Parse "overwrite" extensions --==// if (ext.renderer && !ext.name) { @@ -192,7 +189,7 @@ marked.use = function(extension) { } opts.renderer = renderer; } - if (ext.tokenizer && !ext.before) { + if (ext.tokenizer && !ext.level) { const tokenizer = marked.defaults.tokenizer || new Tokenizer(); for (const prop in ext.tokenizer) { const prevTokenizer = tokenizer[prop]; @@ -219,6 +216,10 @@ marked.use = function(extension) { } }); + if (Object.keys(extensions).length) { + opts.extensions = extensions; + } + marked.setOptions(opts); }; diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index f5d8c42540..b7ab8d3991 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -140,7 +140,6 @@ describe('use extension', () => { it('should use custom block tokenizer + renderer extensions', () => { const underline = { name: 'underline', - before: 'paragraph', // Leave blank to run after everything else...? level: 'block', tokenizer: (src) => { const rule = /^:([^\n]*)(?:\n|$)/; @@ -168,7 +167,6 @@ describe('use extension', () => { it('should interrupt paragraphs if using "start" property', () => { const underline = { name: 'underline', - before: 'paragraph', // Leave blank to run after everything else...? level: 'block', start: /:/, tokenizer: (src) => { @@ -194,7 +192,6 @@ describe('use extension', () => { it('should use custom inline tokenizer + renderer extensions', () => { const underline = { name: 'underline', - before: 'emStrong', // Leave blank to run after everything else...? level: 'inline', start: /=/, tokenizer: (src) => { From b107826b3d4ddfa1468a4a6d759af16978d377ef Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 13 May 2021 01:00:30 -0400 Subject: [PATCH 12/48] Add unit test showing interacting block and inline extensions --- src/Lexer.js | 2 +- test/unit/marked-spec.js | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Lexer.js b/src/Lexer.js index 320ae6b3f8..2c91b32b0e 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -130,7 +130,7 @@ module.exports = class Lexer { if (this.options.extensions?.block) { tokensParsed = false; this.options.extensions.block.forEach(function(extTokenizer, index) { - if (token = extTokenizer(src)) { + if (token = extTokenizer(src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index b7ab8d3991..3b45aba9fb 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -214,6 +214,58 @@ describe('use extension', () => { expect(html).toBe('

Not Underlined Underlined Not Underlined

\n'); }); + it('should handle interacting block and inline extensions', () => { + const descriptionlist = { + name: 'descriptionList', + level: 'block', + start: /:[^:\n]/, + tokenizer: (src) => { + const rule = /^(?::[^:\n]+:[^:\n]*(?:\n|$))+/; + const match = rule.exec(src); + if (match) { + return { + type: 'descriptionList', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[0].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + }, + renderer: (token) => { + return `
${marked.parseInline(token.text)}\n
`; + } + }; + + const description = { + name: 'description', + level: 'inline', + start: /:[^:]/, + tokenizer: (src) => { + const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'description', + raw: match[0], // This is the text that you want your token to consume from the source + dt: match[1].trim(), // You can add additional properties to your tokens to pass along to the renderer + dd: match[2].trim() + }; + } + }, + renderer: (token) => { + return `\n
${marked.parseInline(token.dt)}
${marked.parseInline(token.dd)}
`; + } + }; + marked.use([descriptionlist, description]); + const html = marked('A Description List with One Description:\n' + + ': Topic 1 : Description 1\n' + + ': **Topic 2** : *Description 2*'); + expect(html).toBe('

A Description List with One Description:

\n' + + '
' + + '\n
Topic 1
Description 1
' + + '\n
Topic 2
Description 2
' + + '\n
'); + }); + it('should use renderer', () => { const extension = { renderer: { From 47538e9f059f679af0e85dab4af7e587de38b2ca Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 13 May 2021 15:47:13 -0400 Subject: [PATCH 13/48] Remove TokenParsed from Parser.js --- src/Parser.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Parser.js b/src/Parser.js index 733fc64d0f..51cb241370 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -57,8 +57,7 @@ module.exports = class Parser { item, checked, task, - checkbox, - tokenParsed; + checkbox; const l = tokens.length; for (i = 0; i < l; i++) { @@ -183,12 +182,10 @@ module.exports = class Parser { default: { // Run any renderer extensions - tokenParsed = false; if (this.options.extensions?.[token.type]) { out += this.options.extensions[token.type](token); - tokenParsed = true; + continue; } - if (tokenParsed) continue; const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { @@ -211,8 +208,7 @@ module.exports = class Parser { renderer = renderer || this.renderer; let out = '', i, - token, - tokenParsed; + token; const l = tokens.length; for (i = 0; i < l; i++) { @@ -260,12 +256,10 @@ module.exports = class Parser { } default: { // Run any renderer extensions - tokenParsed = false; if (this.options.extensions?.[token.type]) { out += this.options.extensions[token.type](token); - tokenParsed = true; + continue; } - if (tokenParsed) continue; const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { From 31b7fa595d02890464b732f4268363d98edc937d Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 14 May 2021 00:35:56 -0400 Subject: [PATCH 14/48] Better Paragraph interruption --- src/Lexer.js | 15 +++++++++++++-- test/unit/marked-spec.js | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index 2c91b32b0e..ff2ecd83d5 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -247,12 +247,23 @@ module.exports = class Lexer { cutSrc = src; if (this.options.extensions?.startBlock) { const match = src.match(this.options.extensions.startBlock); - if (match && match.length > 0) { + if (match && match.length > 0 && match.index > 0) { cutSrc = src.substring(0, match.index); } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { + if(cutSrc.length != src.length) { + token.clipped = true; + } src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken?.clipped) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + lastToken.clipped = token.clipped || false; + continue; + } + //console.log(token); tokens.push(token); continue; } @@ -497,7 +508,7 @@ module.exports = class Lexer { cutSrc = src; if (this.options.extensions?.startInline) { const match = src.match(this.options.extensions.startInline); - if (match && match.length > 0) { + if (match && match.length > 0 && match.index > 0) { cutSrc = src.substring(0, match.index); } } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 3b45aba9fb..ac7a70ab3d 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -170,7 +170,7 @@ describe('use extension', () => { level: 'block', start: /:/, tokenizer: (src) => { - const rule = /^:([^\n]*)(?:\n|$)/; + const rule = /^:([^\n]*):(?:\n|$)/; const match = rule.exec(src); if (match) { return { @@ -185,8 +185,8 @@ describe('use extension', () => { } }; marked.use(underline); - const html = marked('Not Underlined\n:Underlined\nNot Underlined'); - expect(html).toBe('

Not Underlined

\nUnderlined\n

Not Underlined

\n'); + const html = marked('Not Underlined A\n:Underlined B:\nNot Underlined C\n:Not Underlined D'); + expect(html).toBe('

Not Underlined A

\nUnderlined B\n

Not Underlined C\n:Not Underlined D

\n'); }); it('should use custom inline tokenizer + renderer extensions', () => { @@ -266,6 +266,33 @@ describe('use extension', () => { + '\n'); }); + it('should allow other options mixed into the extension', () => { + const extension = { + sanitize: true, + silent: true, + name: 'underline', + level: 'block', + start: /:/, + tokenizer: (src) => { + const rule = /^:([^\n]*):(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'underline', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + }, + renderer: (token) => { + return `${token.text}\n`; + } + }; + marked.use(extension); + const html = marked(':test:\ntest\n
'); + expect(html).toBe('test\n

test

\n

<div></div>

\n'); + }); + it('should use renderer', () => { const extension = { renderer: { From a03a64f856d60ccd73741dda4cf60c8bf63686b7 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 14 May 2021 00:44:39 -0400 Subject: [PATCH 15/48] Lint --- lib/marked.esm.js | 31 ++++++++++++++++--------------- src/Lexer.js | 16 ++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/lib/marked.esm.js b/lib/marked.esm.js index 5605cb61cb..92e0ef9c21 100644 --- a/lib/marked.esm.js +++ b/lib/marked.esm.js @@ -1474,14 +1474,14 @@ var Lexer_1 = class Lexer { if (this.options.pedantic) { src = src.replace(/^ +$/gm, ''); } - let token, i, l, lastToken, tokensParsed, cutSrc; + let token, i, l, lastToken, tokensParsed, cutSrc, lastParagraphClipped; while (src) { // extensions if (this.options.extensions?.block) { tokensParsed = false; this.options.extensions.block.forEach(function(extTokenizer, index) { - if (token = extTokenizer(src)) { + if (token = extTokenizer(src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; @@ -1598,13 +1598,20 @@ var Lexer_1 = class Lexer { cutSrc = src; if (this.options.extensions?.startBlock) { const match = src.match(this.options.extensions.startBlock); - if (match && match.length > 0) { + if (match && match.length > 0 && match.index > 0) { cutSrc = src.substring(0, match.index); } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { + lastToken = tokens[tokens.length - 1]; + if (lastParagraphClipped && lastToken.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + } else { + tokens.push(token); + } + lastParagraphClipped = (cutSrc.length !== src.length); src = src.substring(token.raw.length); - tokens.push(token); continue; } @@ -1845,7 +1852,7 @@ var Lexer_1 = class Lexer { cutSrc = src; if (this.options.extensions?.startInline) { const match = src.match(this.options.extensions.startInline); - if (match && match.length > 0) { + if (match && match.length > 0 && match.index > 0) { cutSrc = src.substring(0, match.index); } } @@ -2198,8 +2205,7 @@ var Parser_1 = class Parser { item, checked, task, - checkbox, - tokenParsed; + checkbox; const l = tokens.length; for (i = 0; i < l; i++) { @@ -2324,12 +2330,10 @@ var Parser_1 = class Parser { default: { // Run any renderer extensions - tokenParsed = false; if (this.options.extensions?.[token.type]) { out += this.options.extensions[token.type](token); - tokenParsed = true; + continue; } - if (tokenParsed) continue; const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { @@ -2352,8 +2356,7 @@ var Parser_1 = class Parser { renderer = renderer || this.renderer; let out = '', i, - token, - tokenParsed; + token; const l = tokens.length; for (i = 0; i < l; i++) { @@ -2401,12 +2404,10 @@ var Parser_1 = class Parser { } default: { // Run any renderer extensions - tokenParsed = false; if (this.options.extensions?.[token.type]) { out += this.options.extensions[token.type](token); - tokenParsed = true; + continue; } - if (tokenParsed) continue; const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { diff --git a/src/Lexer.js b/src/Lexer.js index ff2ecd83d5..04e9795e8b 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -123,7 +123,7 @@ module.exports = class Lexer { if (this.options.pedantic) { src = src.replace(/^ +$/gm, ''); } - let token, i, l, lastToken, tokensParsed, cutSrc; + let token, i, l, lastToken, tokensParsed, cutSrc, lastParagraphClipped; while (src) { // extensions @@ -252,19 +252,15 @@ module.exports = class Lexer { } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { - if(cutSrc.length != src.length) { - token.clipped = true; - } - src = src.substring(token.raw.length); lastToken = tokens[tokens.length - 1]; - if (lastToken?.clipped) { + if (lastParagraphClipped && lastToken.type === 'paragraph') { lastToken.raw += '\n' + token.raw; lastToken.text += '\n' + token.text; - lastToken.clipped = token.clipped || false; - continue; + } else { + tokens.push(token); } - //console.log(token); - tokens.push(token); + lastParagraphClipped = (cutSrc.length !== src.length); + src = src.substring(token.raw.length); continue; } From 16fc4099e5b6a9e015dcc81a476925ab3a2788d6 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 14 May 2021 09:05:56 -0400 Subject: [PATCH 16/48] Put renderers in its own property --- src/Parser.js | 8 ++++---- src/marked.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Parser.js b/src/Parser.js index 51cb241370..66bb0251fe 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -182,8 +182,8 @@ module.exports = class Parser { default: { // Run any renderer extensions - if (this.options.extensions?.[token.type]) { - out += this.options.extensions[token.type](token); + if (this.options.extensions.renderers?.[token.type]) { + out += this.options.extensions.renderers[token.type](token); continue; } @@ -256,8 +256,8 @@ module.exports = class Parser { } default: { // Run any renderer extensions - if (this.options.extensions?.[token.type]) { - out += this.options.extensions[token.type](token); + if (this.options.extensions.renderers?.[token.type]) { + out += this.options.extensions.renderers[token.type](token); continue; } diff --git a/src/marked.js b/src/marked.js index fb59edb3c3..ec3f6738d7 100644 --- a/src/marked.js +++ b/src/marked.js @@ -148,12 +148,12 @@ marked.use = function(extension) { const opts = merge({}, ...extension); opts.tokenizer = null; opts.renderer = null; - const extensions = {}; + const extensions = { renderers: {} }; extension.forEach((ext) => { //= =-- Parse "addon" extensions --==// if (ext.renderer && ext.name) { // Renderers must have 'name' property - extensions[ext.name] = ext.renderer; + extensions.renderers[ext.name] = ext.renderer; } if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property if (extensions[ext.level]) { From 15be9182b8239c5a8434bcccd25628c669b8e23e Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 14 May 2021 09:57:09 -0400 Subject: [PATCH 17/48] Unit test for custom renderer that returns false. --- src/Parser.js | 4 ++-- test/unit/marked-spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Parser.js b/src/Parser.js index 66bb0251fe..fc5121ef64 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -183,7 +183,7 @@ module.exports = class Parser { default: { // Run any renderer extensions if (this.options.extensions.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type](token); + out += this.options.extensions.renderers[token.type](token) || ''; continue; } @@ -257,7 +257,7 @@ module.exports = class Parser { default: { // Run any renderer extensions if (this.options.extensions.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type](token); + out += this.options.extensions.renderers[token.type](token) || ''; continue; } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index ac7a70ab3d..33d7ee286c 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -293,6 +293,30 @@ describe('use extension', () => { expect(html).toBe('test\n

test

\n

<div></div>

\n'); }); + it('should handle renderers that return false', () => { + const extension = { + name: 'test', + level: 'block', + tokenizer: (src) => { + const rule = /^:([^\n]*):(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'test', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + }, + renderer: (token) => { + return false; + } + }; + marked.use(extension); + const html = marked(':Test:'); + expect(html).toBe(''); + }); + it('should use renderer', () => { const extension = { renderer: { From 0cb670e601775e53cd2cb59e745b469a060b815c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 14 May 2021 12:05:23 -0400 Subject: [PATCH 18/48] Make 'start' rules into functions. --- src/Lexer.js | 18 ++++++++++++------ src/marked.js | 18 +++++++++++------- test/unit/marked-spec.js | 10 +++++----- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index 04e9795e8b..f929514000 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -246,9 +246,12 @@ module.exports = class Lexer { // prevent paragraph consuming extensions by clipping 'src' to extension start cutSrc = src; if (this.options.extensions?.startBlock) { - const match = src.match(this.options.extensions.startBlock); - if (match && match.length > 0 && match.index > 0) { - cutSrc = src.substring(0, match.index); + let startIndex = Infinity; + this.options.extensions.startBlock.forEach(function(getStartIndex) { + startIndex = Math.max(0, Math.min(getStartIndex(src), startIndex)); + }); + if (startIndex < Infinity && startIndex > 0) { + cutSrc = src.substring(0, startIndex); } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { @@ -503,9 +506,12 @@ module.exports = class Lexer { // prevent inlineText consuming extensions by clipping 'src' to extension start cutSrc = src; if (this.options.extensions?.startInline) { - const match = src.match(this.options.extensions.startInline); - if (match && match.length > 0 && match.index > 0) { - cutSrc = src.substring(0, match.index); + let startIndex = Infinity; + this.options.extensions.startInline.forEach(function(getStartIndex) { + startIndex = Math.max(0, Math.min(getStartIndex(src), startIndex)); + }); + if (startIndex < Infinity && startIndex > 0) { + cutSrc = src.substring(0, startIndex); } } if (token = this.tokenizer.inlineText(cutSrc, inRawBlock, smartypants)) { diff --git a/src/marked.js b/src/marked.js index ec3f6738d7..42a9772308 100644 --- a/src/marked.js +++ b/src/marked.js @@ -161,15 +161,19 @@ marked.use = function(extension) { } else { extensions[ext.level] = [ext.tokenizer]; } - if (ext.start) { // Regex to check for start of token + if (ext.start) { // Function to check for start of token if (ext.level === 'block') { - extensions.startBlock = extensions.startBlock - ? new RegExp(extensions.startBlock.source + '|' + ext.start.source) - : ext.start; + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } else { + extensions.startBlock = [ext.start]; + } } else if (ext.level === 'inline') { - extensions.startInline = extensions.startInline - ? new RegExp(extensions.startInline.source + '|' + ext.start.source) - : ext.start; + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } else { + extensions.startInline = [ext.start]; + } } } } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 33d7ee286c..763bc220ef 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -168,7 +168,7 @@ describe('use extension', () => { const underline = { name: 'underline', level: 'block', - start: /:/, + start: (src) => { return src.match(/:/)?.index; }, tokenizer: (src) => { const rule = /^:([^\n]*):(?:\n|$)/; const match = rule.exec(src); @@ -193,7 +193,7 @@ describe('use extension', () => { const underline = { name: 'underline', level: 'inline', - start: /=/, + start: (src) => { return src.match(/=/)?.index; }, tokenizer: (src) => { const rule = /^=([^=]+)=/; const match = rule.exec(src); @@ -218,7 +218,7 @@ describe('use extension', () => { const descriptionlist = { name: 'descriptionList', level: 'block', - start: /:[^:\n]/, + start: (src) => { return src.match(/:[^:\n]/)?.index; }, tokenizer: (src) => { const rule = /^(?::[^:\n]+:[^:\n]*(?:\n|$))+/; const match = rule.exec(src); @@ -238,7 +238,7 @@ describe('use extension', () => { const description = { name: 'description', level: 'inline', - start: /:[^:]/, + start: (src) => { return src.match(/:/)?.index; }, tokenizer: (src) => { const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; const match = rule.exec(src); @@ -272,7 +272,7 @@ describe('use extension', () => { silent: true, name: 'underline', level: 'block', - start: /:/, + start: (src) => { return src.indexOf(':'); }, tokenizer: (src) => { const rule = /^:([^\n]*):(?:\n|$)/; const match = rule.exec(src); From 6ec699ee147fa24555f7304eab75ee0f918a9791 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 15 May 2021 01:27:05 -0400 Subject: [PATCH 19/48] Extensions can use lexer and parser functions --- lib/marked.esm.js | 56 +++++++++++++++++++++++----------------- src/Lexer.js | 8 +++--- src/Parser.js | 4 +-- test/unit/marked-spec.js | 19 +++++++------- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/lib/marked.esm.js b/lib/marked.esm.js index 92e0ef9c21..d69acf31d5 100644 --- a/lib/marked.esm.js +++ b/lib/marked.esm.js @@ -1480,8 +1480,8 @@ var Lexer_1 = class Lexer { // extensions if (this.options.extensions?.block) { tokensParsed = false; - this.options.extensions.block.forEach(function(extTokenizer, index) { - if (token = extTokenizer(src, tokens)) { + this.options.extensions.block.forEach((extTokenizer) => { + if (token = extTokenizer(src, tokens, this)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; @@ -1597,9 +1597,12 @@ var Lexer_1 = class Lexer { // prevent paragraph consuming extensions by clipping 'src' to extension start cutSrc = src; if (this.options.extensions?.startBlock) { - const match = src.match(this.options.extensions.startBlock); - if (match && match.length > 0 && match.index > 0) { - cutSrc = src.substring(0, match.index); + let startIndex = Infinity; + this.options.extensions.startBlock.forEach(function(getStartIndex) { + startIndex = Math.max(0, Math.min(getStartIndex(src), startIndex)); + }); + if (startIndex < Infinity && startIndex > 0) { + cutSrc = src.substring(0, startIndex); } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { @@ -1745,8 +1748,8 @@ var Lexer_1 = class Lexer { // extensions if (this.options.extensions?.inline) { tokensParsed = false; - this.options.extensions.inline.forEach(function(extTokenizer, index) { - if (token = extTokenizer(src)) { + this.options.extensions.inline.forEach((extTokenizer) => { + if (token = extTokenizer(src, tokens, this)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; @@ -1851,9 +1854,12 @@ var Lexer_1 = class Lexer { // prevent inlineText consuming extensions by clipping 'src' to extension start cutSrc = src; if (this.options.extensions?.startInline) { - const match = src.match(this.options.extensions.startInline); - if (match && match.length > 0 && match.index > 0) { - cutSrc = src.substring(0, match.index); + let startIndex = Infinity; + this.options.extensions.startInline.forEach(function(getStartIndex) { + startIndex = Math.max(0, Math.min(getStartIndex(src), startIndex)); + }); + if (startIndex < Infinity && startIndex > 0) { + cutSrc = src.substring(0, startIndex); } } if (token = this.tokenizer.inlineText(cutSrc, inRawBlock, smartypants)) { @@ -2330,8 +2336,8 @@ var Parser_1 = class Parser { default: { // Run any renderer extensions - if (this.options.extensions?.[token.type]) { - out += this.options.extensions[token.type](token); + if (this.options.extensions.renderers?.[token.type]) { + out += this.options.extensions.renderers[token.type](token, this) || ''; continue; } @@ -2404,8 +2410,8 @@ var Parser_1 = class Parser { } default: { // Run any renderer extensions - if (this.options.extensions?.[token.type]) { - out += this.options.extensions[token.type](token); + if (this.options.extensions.renderers?.[token.type]) { + out += this.options.extensions.renderers[token.type](token, this) || ''; continue; } @@ -2573,12 +2579,12 @@ marked.use = function(extension) { const opts = merge$2({}, ...extension); opts.tokenizer = null; opts.renderer = null; - const extensions = {}; + const extensions = { renderers: {} }; extension.forEach((ext) => { //= =-- Parse "addon" extensions --==// if (ext.renderer && ext.name) { // Renderers must have 'name' property - extensions[ext.name] = ext.renderer; + extensions.renderers[ext.name] = ext.renderer; } if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property if (extensions[ext.level]) { @@ -2586,15 +2592,19 @@ marked.use = function(extension) { } else { extensions[ext.level] = [ext.tokenizer]; } - if (ext.start) { // Regex to check for start of token + if (ext.start) { // Function to check for start of token if (ext.level === 'block') { - extensions.startBlock = extensions.startBlock - ? new RegExp(extensions.startBlock.source + '|' + ext.start.source) - : ext.start; + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } else { + extensions.startBlock = [ext.start]; + } } else if (ext.level === 'inline') { - extensions.startInline = extensions.startInline - ? new RegExp(extensions.startInline.source + '|' + ext.start.source) - : ext.start; + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } else { + extensions.startInline = [ext.start]; + } } } } diff --git a/src/Lexer.js b/src/Lexer.js index f929514000..87edef02ba 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -129,8 +129,8 @@ module.exports = class Lexer { // extensions if (this.options.extensions?.block) { tokensParsed = false; - this.options.extensions.block.forEach(function(extTokenizer, index) { - if (token = extTokenizer(src, tokens)) { + this.options.extensions.block.forEach((extTokenizer) => { + if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; @@ -400,8 +400,8 @@ module.exports = class Lexer { // extensions if (this.options.extensions?.inline) { tokensParsed = false; - this.options.extensions.inline.forEach(function(extTokenizer, index) { - if (token = extTokenizer(src)) { + this.options.extensions.inline.forEach((extTokenizer) => { + if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; diff --git a/src/Parser.js b/src/Parser.js index fc5121ef64..d300ae02aa 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -183,7 +183,7 @@ module.exports = class Parser { default: { // Run any renderer extensions if (this.options.extensions.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type](token) || ''; + out += this.options.extensions.renderers[token.type].call(this, token) || ''; continue; } @@ -257,7 +257,7 @@ module.exports = class Parser { default: { // Run any renderer extensions if (this.options.extensions.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type](token) || ''; + out += this.options.extensions.renderers[token.type].call(this, token) || ''; continue; } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 763bc220ef..249a088067 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -219,19 +219,20 @@ describe('use extension', () => { name: 'descriptionList', level: 'block', start: (src) => { return src.match(/:[^:\n]/)?.index; }, - tokenizer: (src) => { + tokenizer(src, tokens) { const rule = /^(?::[^:\n]+:[^:\n]*(?:\n|$))+/; const match = rule.exec(src); if (match) { return { type: 'descriptionList', raw: match[0], // This is the text that you want your token to consume from the source - text: match[0].trim() // You can add additional properties to your tokens to pass along to the renderer + text: match[0].trim(), // You can add additional properties to your tokens to pass along to the renderer + tokens: this.inlineTokens(match[0].trim()) }; } }, - renderer: (token) => { - return `
${marked.parseInline(token.text)}\n
`; + renderer(token) { + return `
${this.parseInline(token.tokens)}\n
`; } }; @@ -239,20 +240,20 @@ describe('use extension', () => { name: 'description', level: 'inline', start: (src) => { return src.match(/:/)?.index; }, - tokenizer: (src) => { + tokenizer(src, tokens) { const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; const match = rule.exec(src); if (match) { return { type: 'description', raw: match[0], // This is the text that you want your token to consume from the source - dt: match[1].trim(), // You can add additional properties to your tokens to pass along to the renderer - dd: match[2].trim() + dt: this.inlineTokens(match[1].trim()), // You can add additional properties to your tokens to pass along to the renderer + dd: this.inlineTokens(match[2].trim()) }; } }, - renderer: (token) => { - return `\n
${marked.parseInline(token.dt)}
${marked.parseInline(token.dd)}
`; + renderer(token) { + return `\n
${this.parseInline(token.dt)}
${this.parseInline(token.dd)}
`; } }; marked.use([descriptionlist, description]); From cc23bc996d4df054aebb7d074d7dbe9c0df37862 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 15 May 2021 01:46:38 -0400 Subject: [PATCH 20/48] add walkableTokens property to extensions function that will return an array of any child tokens that should be reachable by `walkTokens`, in case they are not all included in a `.tokens` property. --- src/marked.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/marked.js b/src/marked.js index 42a9772308..a2ba66ed4a 100644 --- a/src/marked.js +++ b/src/marked.js @@ -155,6 +155,9 @@ marked.use = function(extension) { if (ext.renderer && ext.name) { // Renderers must have 'name' property extensions.renderers[ext.name] = ext.renderer; } + if (ext.walkableTokens && ext.name) { // walkableTokens must have 'name' + extensions.walkableTokens[ext.name] = ext.walkableTokens; + } if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property if (extensions[ext.level]) { extensions[ext.level].push(ext.tokenizer); @@ -251,6 +254,11 @@ marked.walkTokens = function(tokens, callback) { break; } default: { + if (this.options.extensions?.walkableTokens) { // Walk any extensions + this.options.extensions.walkableTokens.forEach((walkableTokens) => { + marked.walkTokens(walkableTokens, callback); + }); + } if (token.tokens) { marked.walkTokens(token.tokens, callback); } From 375255ecfef4c7f3ee8c1a55aa2e68af5738b653 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 15 May 2021 01:58:28 -0400 Subject: [PATCH 21/48] Add `walkableTokens` option for extensions. --- src/Parser.js | 4 ++-- src/marked.js | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Parser.js b/src/Parser.js index d300ae02aa..a09c3ace36 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -182,7 +182,7 @@ module.exports = class Parser { default: { // Run any renderer extensions - if (this.options.extensions.renderers?.[token.type]) { + if (this.options.extensions?.renderers?.[token.type]) { out += this.options.extensions.renderers[token.type].call(this, token) || ''; continue; } @@ -256,7 +256,7 @@ module.exports = class Parser { } default: { // Run any renderer extensions - if (this.options.extensions.renderers?.[token.type]) { + if (this.options.extensions?.renderers?.[token.type]) { out += this.options.extensions.renderers[token.type].call(this, token) || ''; continue; } diff --git a/src/marked.js b/src/marked.js index a2ba66ed4a..39eb156e00 100644 --- a/src/marked.js +++ b/src/marked.js @@ -254,10 +254,8 @@ marked.walkTokens = function(tokens, callback) { break; } default: { - if (this.options.extensions?.walkableTokens) { // Walk any extensions - this.options.extensions.walkableTokens.forEach((walkableTokens) => { - marked.walkTokens(walkableTokens, callback); - }); + if (this.options.extensions?.walkableTokens?.[token.type]) { // Walk any extensions + marked.walkTokens(this.options.extensions.walkableTokens[token.type], callback); } if (token.tokens) { marked.walkTokens(token.tokens, callback); From b602e711a61c56b5291864889a1eab5e0933a90e Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 20:41:14 -0400 Subject: [PATCH 22/48] walkable Tokens requires options passed in --- src/marked.js | 26 +++++++++++++++++--------- test/unit/marked-spec.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/marked.js b/src/marked.js index 39eb156e00..43dadddc5c 100644 --- a/src/marked.js +++ b/src/marked.js @@ -108,7 +108,7 @@ function marked(src, opt, callback) { try { const tokens = Lexer.lex(src, opt); if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens); + marked.walkTokens(tokens, opt.walkTokens, opt); } return Parser.parse(tokens, opt); } catch (e) { @@ -148,7 +148,7 @@ marked.use = function(extension) { const opts = merge({}, ...extension); opts.tokenizer = null; opts.renderer = null; - const extensions = { renderers: {} }; + const extensions = { renderers: {}, walkableTokens: {} }; extension.forEach((ext) => { //= =-- Parse "addon" extensions --==// @@ -234,31 +234,39 @@ marked.use = function(extension) { * Run callback for every token */ -marked.walkTokens = function(tokens, callback) { +marked.walkTokens = function(tokens, callback, opt) { for (const token of tokens) { callback(token); switch (token.type) { case 'table': { for (const cell of token.tokens.header) { - marked.walkTokens(cell, callback); + marked.walkTokens(cell, callback, opt); } for (const row of token.tokens.cells) { for (const cell of row) { - marked.walkTokens(cell, callback); + marked.walkTokens(cell, callback, opt); } } break; } case 'list': { - marked.walkTokens(token.items, callback); + marked.walkTokens(token.items, callback, opt); break; } default: { - if (this.options.extensions?.walkableTokens?.[token.type]) { // Walk any extensions - marked.walkTokens(this.options.extensions.walkableTokens[token.type], callback); + if (token.type === 'walkableDescription') { + // console.log(opt); + } + if (opt?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions + // console.log(opt.extensions.walkableTokens[token.type]); + opt.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { + if (walkableTokens !== 'tokens') { + marked.walkTokens(token[walkableTokens], callback, opt); + } + }); } if (token.tokens) { - marked.walkTokens(token.tokens, callback); + marked.walkTokens(token.tokens, callback, opt); } } } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 249a088067..fb40b0e2ac 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -318,6 +318,40 @@ describe('use extension', () => { expect(html).toBe(''); }); + it('should handle list of walkable tokens', () => { + const walkableDescription = { + name: 'walkableDescription', + level: 'inline', + start: (src) => { return src.match(/:/)?.index; }, + tokenizer(src, tokens) { + const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'walkableDescription', + raw: match[0], // This is the text that you want your token to consume from the source + dt: this.inlineTokens(match[1].trim()), // You can add additional properties to your tokens to pass along to the renderer + dd: this.inlineTokens(match[2].trim()) + }; + } + }, + renderer(token) { + return `\n
${this.parseInline(token.dt)}
${this.parseInline(token.dd)}
`; + }, + walkableTokens: ['dd', 'dt'], + walkTokens(token) { + if (token.type === 'text') { + token.text += 'A'; + } + } + }; + marked.use(walkableDescription); + const html = marked(': Topic 1 : Description 1\n' + + ': **Topic 2** : *Description 2*'); + expect(html).toBe('

\n

Topic 1A
Description 1A
' + + '\n
Topic 2A
Description 2A

\n'); + }); + it('should use renderer', () => { const extension = { renderer: { From 9c9c557e2a6648eb6db3766bf00ccd14ffa3c4a4 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 10:53:19 -0400 Subject: [PATCH 23/48] Update src/marked.js Co-authored-by: Tony Brix --- src/marked.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marked.js b/src/marked.js index 43dadddc5c..1cbe1006c5 100644 --- a/src/marked.js +++ b/src/marked.js @@ -151,7 +151,7 @@ marked.use = function(extension) { const extensions = { renderers: {}, walkableTokens: {} }; extension.forEach((ext) => { - //= =-- Parse "addon" extensions --==// + //==-- Parse "addon" extensions --==// if (ext.renderer && ext.name) { // Renderers must have 'name' property extensions.renderers[ext.name] = ext.renderer; } From eccaff43c6b0dc2adf04b4325ed1ca9589dff6a1 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 10:53:31 -0400 Subject: [PATCH 24/48] Update src/marked.js Co-authored-by: Tony Brix --- src/marked.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marked.js b/src/marked.js index 1cbe1006c5..3577cef6dc 100644 --- a/src/marked.js +++ b/src/marked.js @@ -181,7 +181,7 @@ marked.use = function(extension) { } } - //= =-- Parse "overwrite" extensions --==// + //==-- Parse "overwrite" extensions --==// if (ext.renderer && !ext.name) { const renderer = marked.defaults.renderer || new Renderer(); for (const prop in ext.renderer) { From adace8cd0df8a2cddbde0ea940af48ad3d547afd Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 20:44:34 -0400 Subject: [PATCH 25/48] lint --- src/marked.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/marked.js b/src/marked.js index 3577cef6dc..59bcdc60e5 100644 --- a/src/marked.js +++ b/src/marked.js @@ -151,7 +151,7 @@ marked.use = function(extension) { const extensions = { renderers: {}, walkableTokens: {} }; extension.forEach((ext) => { - //==-- Parse "addon" extensions --==// + // ==-- Parse "addon" extensions --== // if (ext.renderer && ext.name) { // Renderers must have 'name' property extensions.renderers[ext.name] = ext.renderer; } @@ -181,7 +181,7 @@ marked.use = function(extension) { } } - //==-- Parse "overwrite" extensions --==// + // ==-- Parse "overwrite" extensions --== // if (ext.renderer && !ext.name) { const renderer = marked.defaults.renderer || new Renderer(); for (const prop in ext.renderer) { @@ -211,7 +211,7 @@ marked.use = function(extension) { opts.tokenizer = tokenizer; } - //= =-- Parse WalkTokens extensions --==// + // ==-- Parse WalkTokens extensions --== // if (ext.walkTokens) { const walkTokens = marked.defaults.walkTokens; opts.walkTokens = (token) => { From 7fafa71817ee5f2fb8db3bb30850b75718533378 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 21:59:36 -0400 Subject: [PATCH 26/48] Update unit test functions to object method syntax --- lib/marked.esm.js | 107 +++++++++++++++++++++---------------- src/marked.js | 73 +++++++++++++------------- test/unit/marked-spec.js | 110 ++++++++++++++++++++------------------- 3 files changed, 154 insertions(+), 136 deletions(-) diff --git a/lib/marked.esm.js b/lib/marked.esm.js index d69acf31d5..a3ec1ad3f1 100644 --- a/lib/marked.esm.js +++ b/lib/marked.esm.js @@ -1481,7 +1481,7 @@ var Lexer_1 = class Lexer { if (this.options.extensions?.block) { tokensParsed = false; this.options.extensions.block.forEach((extTokenizer) => { - if (token = extTokenizer(src, tokens, this)) { + if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; @@ -1749,7 +1749,7 @@ var Lexer_1 = class Lexer { if (this.options.extensions?.inline) { tokensParsed = false; this.options.extensions.inline.forEach((extTokenizer) => { - if (token = extTokenizer(src, tokens, this)) { + if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); tokensParsed = true; @@ -2336,8 +2336,8 @@ var Parser_1 = class Parser { default: { // Run any renderer extensions - if (this.options.extensions.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type](token, this) || ''; + if (this.options.extensions?.renderers?.[token.type]) { + out += this.options.extensions.renderers[token.type].call(this, token) || ''; continue; } @@ -2410,8 +2410,8 @@ var Parser_1 = class Parser { } default: { // Run any renderer extensions - if (this.options.extensions.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type](token, this) || ''; + if (this.options.extensions?.renderers?.[token.type]) { + out += this.options.extensions.renderers[token.type].call(this, token) || ''; continue; } @@ -2539,7 +2539,7 @@ function marked(src, opt, callback) { try { const tokens = Lexer.lex(src, opt); if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens); + marked.walkTokens(tokens, opt.walkTokens, opt); } return Parser.parse(tokens, opt); } catch (e) { @@ -2579,43 +2579,51 @@ marked.use = function(extension) { const opts = merge$2({}, ...extension); opts.tokenizer = null; opts.renderer = null; - const extensions = { renderers: {} }; - - extension.forEach((ext) => { - //= =-- Parse "addon" extensions --==// - if (ext.renderer && ext.name) { // Renderers must have 'name' property - extensions.renderers[ext.name] = ext.renderer; - } - if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property - if (extensions[ext.level]) { - extensions[ext.level].push(ext.tokenizer); - } else { - extensions[ext.level] = [ext.tokenizer]; - } - if (ext.start) { // Function to check for start of token - if (ext.level === 'block') { - if (extensions.startBlock) { - extensions.startBlock.push(ext.start); + opts.extensions = null; + const extensions = { renderers: {}, walkableTokens: {} }; + + extension.forEach((pack) => { + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (ext.renderer && ext.name) { // Renderers must have 'name' property + extensions.renderers[ext.name] = ext.renderer; + } + if (ext.walkableTokens && ext.name) { // walkableTokens must have 'name' + extensions.walkableTokens[ext.name] = ext.walkableTokens; + } + if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property + if (extensions[ext.level]) { + extensions[ext.level].push(ext.tokenizer); } else { - extensions.startBlock = [ext.start]; + extensions[ext.level] = [ext.tokenizer]; } - } else if (ext.level === 'inline') { - if (extensions.startInline) { - extensions.startInline.push(ext.start); - } else { - extensions.startInline = [ext.start]; + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } else { + extensions.startBlock = [ext.start]; + } + } else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } else { + extensions.startInline = [ext.start]; + } + } } } - } + }); } - //= =-- Parse "overwrite" extensions --==// - if (ext.renderer && !ext.name) { + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { const renderer = marked.defaults.renderer || new Renderer_1(); - for (const prop in ext.renderer) { + for (const prop in pack.renderer) { const prevRenderer = renderer[prop]; renderer[prop] = (...args) => { - let ret = ext.renderer[prop].apply(renderer, args); + let ret = pack.renderer[prop].apply(renderer, args); if (ret === false) { ret = prevRenderer.apply(renderer, args); } @@ -2624,12 +2632,12 @@ marked.use = function(extension) { } opts.renderer = renderer; } - if (ext.tokenizer && !ext.level) { + if (pack.tokenizer) { const tokenizer = marked.defaults.tokenizer || new Tokenizer_1(); - for (const prop in ext.tokenizer) { + for (const prop in pack.tokenizer) { const prevTokenizer = tokenizer[prop]; tokenizer[prop] = (...args) => { - let ret = ext.tokenizer[prop].apply(tokenizer, args); + let ret = pack.tokenizer[prop].apply(tokenizer, args); if (ret === false) { ret = prevTokenizer.apply(tokenizer, args); } @@ -2639,11 +2647,11 @@ marked.use = function(extension) { opts.tokenizer = tokenizer; } - //= =-- Parse WalkTokens extensions --==// - if (ext.walkTokens) { + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { const walkTokens = marked.defaults.walkTokens; opts.walkTokens = (token) => { - ext.walkTokens(token); + pack.walkTokens(token); if (walkTokens) { walkTokens(token); } @@ -2662,28 +2670,35 @@ marked.use = function(extension) { * Run callback for every token */ -marked.walkTokens = function(tokens, callback) { +marked.walkTokens = function(tokens, callback, opt) { for (const token of tokens) { callback(token); switch (token.type) { case 'table': { for (const cell of token.tokens.header) { - marked.walkTokens(cell, callback); + marked.walkTokens(cell, callback, opt); } for (const row of token.tokens.cells) { for (const cell of row) { - marked.walkTokens(cell, callback); + marked.walkTokens(cell, callback, opt); } } break; } case 'list': { - marked.walkTokens(token.items, callback); + marked.walkTokens(token.items, callback, opt); break; } default: { + if (opt?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions + opt.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { + if (walkableTokens !== 'tokens') { + marked.walkTokens(token[walkableTokens], callback, opt); + } + }); + } if (token.tokens) { - marked.walkTokens(token.tokens, callback); + marked.walkTokens(token.tokens, callback, opt); } } } diff --git a/src/marked.js b/src/marked.js index 59bcdc60e5..e35b9f0d4f 100644 --- a/src/marked.js +++ b/src/marked.js @@ -148,46 +148,51 @@ marked.use = function(extension) { const opts = merge({}, ...extension); opts.tokenizer = null; opts.renderer = null; + opts.extensions = null; const extensions = { renderers: {}, walkableTokens: {} }; - extension.forEach((ext) => { + extension.forEach((pack) => { // ==-- Parse "addon" extensions --== // - if (ext.renderer && ext.name) { // Renderers must have 'name' property - extensions.renderers[ext.name] = ext.renderer; - } - if (ext.walkableTokens && ext.name) { // walkableTokens must have 'name' - extensions.walkableTokens[ext.name] = ext.walkableTokens; - } - if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property - if (extensions[ext.level]) { - extensions[ext.level].push(ext.tokenizer); - } else { - extensions[ext.level] = [ext.tokenizer]; - } - if (ext.start) { // Function to check for start of token - if (ext.level === 'block') { - if (extensions.startBlock) { - extensions.startBlock.push(ext.start); + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (ext.renderer && ext.name) { // Renderers must have 'name' property + extensions.renderers[ext.name] = ext.renderer; + } + if (ext.walkableTokens && ext.name) { // walkableTokens must have 'name' + extensions.walkableTokens[ext.name] = ext.walkableTokens; + } + if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property + if (extensions[ext.level]) { + extensions[ext.level].push(ext.tokenizer); } else { - extensions.startBlock = [ext.start]; + extensions[ext.level] = [ext.tokenizer]; } - } else if (ext.level === 'inline') { - if (extensions.startInline) { - extensions.startInline.push(ext.start); - } else { - extensions.startInline = [ext.start]; + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } else { + extensions.startBlock = [ext.start]; + } + } else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } else { + extensions.startInline = [ext.start]; + } + } } } - } + }); } // ==-- Parse "overwrite" extensions --== // - if (ext.renderer && !ext.name) { + if (pack.renderer) { const renderer = marked.defaults.renderer || new Renderer(); - for (const prop in ext.renderer) { + for (const prop in pack.renderer) { const prevRenderer = renderer[prop]; renderer[prop] = (...args) => { - let ret = ext.renderer[prop].apply(renderer, args); + let ret = pack.renderer[prop].apply(renderer, args); if (ret === false) { ret = prevRenderer.apply(renderer, args); } @@ -196,12 +201,12 @@ marked.use = function(extension) { } opts.renderer = renderer; } - if (ext.tokenizer && !ext.level) { + if (pack.tokenizer) { const tokenizer = marked.defaults.tokenizer || new Tokenizer(); - for (const prop in ext.tokenizer) { + for (const prop in pack.tokenizer) { const prevTokenizer = tokenizer[prop]; tokenizer[prop] = (...args) => { - let ret = ext.tokenizer[prop].apply(tokenizer, args); + let ret = pack.tokenizer[prop].apply(tokenizer, args); if (ret === false) { ret = prevTokenizer.apply(tokenizer, args); } @@ -212,10 +217,10 @@ marked.use = function(extension) { } // ==-- Parse WalkTokens extensions --== // - if (ext.walkTokens) { + if (pack.walkTokens) { const walkTokens = marked.defaults.walkTokens; opts.walkTokens = (token) => { - ext.walkTokens(token); + pack.walkTokens(token); if (walkTokens) { walkTokens(token); } @@ -254,11 +259,7 @@ marked.walkTokens = function(tokens, callback, opt) { break; } default: { - if (token.type === 'walkableDescription') { - // console.log(opt); - } if (opt?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions - // console.log(opt.extensions.walkableTokens[token.type]); opt.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { if (walkableTokens !== 'tokens') { marked.walkTokens(token[walkableTokens], callback, opt); diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index fb40b0e2ac..a94a0583eb 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -141,7 +141,7 @@ describe('use extension', () => { const underline = { name: 'underline', level: 'block', - tokenizer: (src) => { + tokenizer(src) { const rule = /^:([^\n]*)(?:\n|$)/; const match = rule.exec(src); if (match) { @@ -152,11 +152,11 @@ describe('use extension', () => { }; } }, - renderer: (token) => { + renderer(token) { return `${token.text}\n`; } }; - marked.use(underline); + marked.use({ extensions: [underline] }); let html = marked('Not Underlined\n:Underlined\nNot Underlined'); expect(html).toBe('

Not Underlined\n:Underlined\nNot Underlined

\n'); @@ -166,23 +166,25 @@ describe('use extension', () => { it('should interrupt paragraphs if using "start" property', () => { const underline = { - name: 'underline', - level: 'block', - start: (src) => { return src.match(/:/)?.index; }, - tokenizer: (src) => { - const rule = /^:([^\n]*):(?:\n|$)/; - const match = rule.exec(src); - if (match) { - return { - type: 'underline', - raw: match[0], // This is the text that you want your token to consume from the source - text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer - }; + extensions: [{ + name: 'underline', + level: 'block', + start(src) { return src.match(/:/)?.index; }, + tokenizer(src) { + const rule = /^:([^\n]*):(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'underline', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + }, + renderer(token) { + return `${token.text}\n`; } - }, - renderer: (token) => { - return `${token.text}\n`; - } + }] }; marked.use(underline); const html = marked('Not Underlined A\n:Underlined B:\nNot Underlined C\n:Not Underlined D'); @@ -193,8 +195,8 @@ describe('use extension', () => { const underline = { name: 'underline', level: 'inline', - start: (src) => { return src.match(/=/)?.index; }, - tokenizer: (src) => { + start(src) { return src.match(/=/)?.index; }, + tokenizer(src) { const rule = /^=([^=]+)=/; const match = rule.exec(src); if (match) { @@ -205,11 +207,11 @@ describe('use extension', () => { }; } }, - renderer: (token) => { + renderer(token) { return `${token.text}`; } }; - marked.use(underline); + marked.use({ extensions: [underline] }); const html = marked('Not Underlined =Underlined= Not Underlined'); expect(html).toBe('

Not Underlined Underlined Not Underlined

\n'); }); @@ -218,7 +220,7 @@ describe('use extension', () => { const descriptionlist = { name: 'descriptionList', level: 'block', - start: (src) => { return src.match(/:[^:\n]/)?.index; }, + start(src) { return src.match(/:[^:\n]/)?.index; }, tokenizer(src, tokens) { const rule = /^(?::[^:\n]+:[^:\n]*(?:\n|$))+/; const match = rule.exec(src); @@ -239,7 +241,7 @@ describe('use extension', () => { const description = { name: 'description', level: 'inline', - start: (src) => { return src.match(/:/)?.index; }, + start(src) { return src.match(/:/)?.index; }, tokenizer(src, tokens) { const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; const match = rule.exec(src); @@ -256,7 +258,7 @@ describe('use extension', () => { return `\n
${this.parseInline(token.dt)}
${this.parseInline(token.dd)}
`; } }; - marked.use([descriptionlist, description]); + marked.use({ extensions: [descriptionlist, description] }); const html = marked('A Description List with One Description:\n' + ': Topic 1 : Description 1\n' + ': **Topic 2** : *Description 2*'); @@ -269,12 +271,10 @@ describe('use extension', () => { it('should allow other options mixed into the extension', () => { const extension = { - sanitize: true, - silent: true, name: 'underline', level: 'block', - start: (src) => { return src.indexOf(':'); }, - tokenizer: (src) => { + start(src) { return src.indexOf(':'); }, + tokenizer(src) { const rule = /^:([^\n]*):(?:\n|$)/; const match = rule.exec(src); if (match) { @@ -285,11 +285,11 @@ describe('use extension', () => { }; } }, - renderer: (token) => { + renderer(token) { return `${token.text}\n`; } }; - marked.use(extension); + marked.use({ sanitize: true, silent: true, extensions: [extension] }); const html = marked(':test:\ntest\n
'); expect(html).toBe('test\n

test

\n

<div></div>

\n'); }); @@ -298,7 +298,7 @@ describe('use extension', () => { const extension = { name: 'test', level: 'block', - tokenizer: (src) => { + tokenizer(src) { const rule = /^:([^\n]*):(?:\n|$)/; const match = rule.exec(src); if (match) { @@ -309,36 +309,38 @@ describe('use extension', () => { }; } }, - renderer: (token) => { + renderer(token) { return false; } }; - marked.use(extension); + marked.use({ extensions: [extension] }); const html = marked(':Test:'); expect(html).toBe(''); }); it('should handle list of walkable tokens', () => { const walkableDescription = { - name: 'walkableDescription', - level: 'inline', - start: (src) => { return src.match(/:/)?.index; }, - tokenizer(src, tokens) { - const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; - const match = rule.exec(src); - if (match) { - return { - type: 'walkableDescription', - raw: match[0], // This is the text that you want your token to consume from the source - dt: this.inlineTokens(match[1].trim()), // You can add additional properties to your tokens to pass along to the renderer - dd: this.inlineTokens(match[2].trim()) - }; - } - }, - renderer(token) { - return `\n
${this.parseInline(token.dt)}
${this.parseInline(token.dd)}
`; - }, - walkableTokens: ['dd', 'dt'], + extensions: [{ + name: 'walkableDescription', + level: 'inline', + start(src) { return src.match(/:/)?.index; }, + tokenizer(src, tokens) { + const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'walkableDescription', + raw: match[0], // This is the text that you want your token to consume from the source + dt: this.inlineTokens(match[1].trim()), // You can add additional properties to your tokens to pass along to the renderer + dd: this.inlineTokens(match[2].trim()) + }; + } + }, + renderer(token) { + return `\n
${this.parseInline(token.dt)}
${this.parseInline(token.dd)}
`; + }, + walkableTokens: ['dd', 'dt'] + }], walkTokens(token) { if (token.type === 'text') { token.text += 'A'; From e993545b67f1e123ce1f1008eda52463a045df35 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 23:07:18 -0400 Subject: [PATCH 27/48] walktokens uses marked.defaults instead of opts parameter --- src/marked.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/marked.js b/src/marked.js index e35b9f0d4f..8e443f6d6f 100644 --- a/src/marked.js +++ b/src/marked.js @@ -239,35 +239,35 @@ marked.use = function(extension) { * Run callback for every token */ -marked.walkTokens = function(tokens, callback, opt) { +marked.walkTokens = function(tokens, callback) { for (const token of tokens) { callback(token); switch (token.type) { case 'table': { for (const cell of token.tokens.header) { - marked.walkTokens(cell, callback, opt); + marked.walkTokens(cell, callback); } for (const row of token.tokens.cells) { for (const cell of row) { - marked.walkTokens(cell, callback, opt); + marked.walkTokens(cell, callback); } } break; } case 'list': { - marked.walkTokens(token.items, callback, opt); + marked.walkTokens(token.items, callback); break; } default: { - if (opt?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions - opt.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { + if (marked.defaults?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions + marked.defaults?.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { if (walkableTokens !== 'tokens') { - marked.walkTokens(token[walkableTokens], callback, opt); + marked.walkTokens(token[walkableTokens], callback); } }); } if (token.tokens) { - marked.walkTokens(token.tokens, callback, opt); + marked.walkTokens(token.tokens, callback); } } } From 95ceb59a07b090d2922285e1558458d5c1acbf52 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 23:12:52 -0400 Subject: [PATCH 28/48] Change hasExtensions to a boolean --- src/marked.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/marked.js b/src/marked.js index 8e443f6d6f..4b9e2b1178 100644 --- a/src/marked.js +++ b/src/marked.js @@ -150,10 +150,12 @@ marked.use = function(extension) { opts.renderer = null; opts.extensions = null; const extensions = { renderers: {}, walkableTokens: {} }; + let hasExtensions; extension.forEach((pack) => { // ==-- Parse "addon" extensions --== // if (pack.extensions) { + hasExtensions = true; pack.extensions.forEach((ext) => { if (ext.renderer && ext.name) { // Renderers must have 'name' property extensions.renderers[ext.name] = ext.renderer; @@ -228,7 +230,7 @@ marked.use = function(extension) { } }); - if (Object.keys(extensions).length) { + if (hasExtensions) { opts.extensions = extensions; } From 70d3e2bc52a5248cc65cbdc8ff1d60f54e5635a0 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 18 May 2021 23:20:24 -0400 Subject: [PATCH 29/48] Don't walk .tokens param if defined in the extension --- src/marked.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/marked.js b/src/marked.js index 4b9e2b1178..85be707cde 100644 --- a/src/marked.js +++ b/src/marked.js @@ -263,12 +263,10 @@ marked.walkTokens = function(tokens, callback) { default: { if (marked.defaults?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions marked.defaults?.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { - if (walkableTokens !== 'tokens') { - marked.walkTokens(token[walkableTokens], callback); - } + marked.walkTokens(token[walkableTokens], callback); }); } - if (token.tokens) { + if (token.tokens && !marked.defaults?.extensions?.walkableTokens[token.type]?.tokens) { marked.walkTokens(token.tokens, callback); } } From 892ef653bc747789e71c948c34894ee153430303 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 19 May 2021 09:54:16 -0400 Subject: [PATCH 30/48] Update Lexer.js --- src/Lexer.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index 87edef02ba..82bd8f20ff 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -123,20 +123,19 @@ module.exports = class Lexer { if (this.options.pedantic) { src = src.replace(/^ +$/gm, ''); } - let token, i, l, lastToken, tokensParsed, cutSrc, lastParagraphClipped; + let token, i, l, lastToken, cutSrc, lastParagraphClipped; while (src) { - // extensions - if (this.options.extensions?.block) { - tokensParsed = false; - this.options.extensions.block.forEach((extTokenizer) => { + if (this.options?.extensions?.block + && this.options.extensions.block.some((extTokenizer) => { if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); - tokensParsed = true; + return true; } - }); - if (tokensParsed) { continue; } + return false; + })) { + continue; } // newline @@ -368,7 +367,7 @@ module.exports = class Lexer { // String with links masked to avoid interference with em and strong let maskedSrc = src; let match; - let keepPrevChar, prevChar, tokensParsed; + let keepPrevChar, prevChar; // Mask out reflinks if (this.tokens.links) { @@ -398,16 +397,16 @@ module.exports = class Lexer { keepPrevChar = false; // extensions - if (this.options.extensions?.inline) { - tokensParsed = false; - this.options.extensions.inline.forEach((extTokenizer) => { + if (this.options?.extensions?.inline + && this.options.extensions.inline.some((extTokenizer) => { if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); - tokensParsed = true; + return true; } - }); - if (tokensParsed) { continue; } + return false; + })) { + continue; } // escape From fa7aaca047ff5f07bbf0f8c18bdf12e609a37948 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Wed, 19 May 2021 11:11:16 -0500 Subject: [PATCH 31/48] add multiple extension test --- test/unit/marked-spec.js | 108 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index a94a0583eb..48c5fc19f8 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -354,6 +354,114 @@ describe('use extension', () => { + '\n
Topic 2A
Description 2A

\n'); }); + describe('multiple extensions', () => { + function createExtension(name) { + return { + extensions: [{ + name: `block-${name}`, + level: 'block', + start(src) { return src.indexOf('::'); }, + tokenizer(src, tokens) { + if (src.startsWith(`::${name}\n`)) { + const text = `:${name}`; + return { + type: `block-${name}`, + raw: `::${name}\n`, + text, + tokens: this.inlineTokens(text) + }; + } + }, + renderer(token) { + return `<${token.type}>${token.text}\n`; + } + }, { + name: `inline-${name}`, + level: 'inline', + start(src) { return src.indexOf(':'); }, + tokenizer(src, tokens) { + if (src.startsWith(`:${name}`)) { + return { + type: `inline-${name}`, + raw: `:${name}`, + text: `used ${name}` + }; + } + }, + renderer(token) { + return token.text; + } + }], + tokenizer: { + heading(src) { + if (src.startsWith(`# ${name}`)) { + return { + type: 'heading', + raw: `# ${name}`, + text: `used ${name}` + }; + } + return false; + } + }, + renderer: { + heading(text, level, raw, slugger) { + if (text === name) { + return `${text}\n`; + } + return false; + } + }, + walkTokens(token) { + if (token.text === `used ${name}`) { + token.text += ' walked'; + } + } + }; + } + + function runTest() { + const html = marked(` +::extension1 +::extension2 + +:extension1 +:extension2 + +# extension1 + +# extension2 + +# no extension +`); + expect(`\n${html}\n`.replace(/\n+/, '\n')).toBe(` +used extension1 walked +used extension2 walked +

used extension1 walked +used extension2 walked

+

used extension1 walked

+

used extension2 walked

+

no extension

+`); + } + + it('should merge extensions when calling marked.use multiple times', () => { + marked.use(createExtension('extension1')); + marked.use(createExtension('extension2')); + + runTest(); + }); + + it('should merge extensions when calling marked.use with multiple extensions', () => { + marked.use([ + createExtension('extension1'), + createExtension('extension2') + ]); + + runTest(); + }); + }); + it('should use renderer', () => { const extension = { renderer: { From ec85ef36b44702a58111ce117873b910b0c04c32 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 19 May 2021 12:51:54 -0400 Subject: [PATCH 32/48] Update src/marked.js Co-authored-by: Tony Brix --- src/marked.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marked.js b/src/marked.js index 85be707cde..932c3afab7 100644 --- a/src/marked.js +++ b/src/marked.js @@ -108,7 +108,7 @@ function marked(src, opt, callback) { try { const tokens = Lexer.lex(src, opt); if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens, opt); + marked.walkTokens(tokens, opt.walkTokens); } return Parser.parse(tokens, opt); } catch (e) { From 8a25d94984758322b244e4b4bfd93089bc2db428 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 20 May 2021 10:21:43 -0400 Subject: [PATCH 33/48] Fix multiple extensions overwriting each other --- src/Lexer.js | 9 ++++++--- src/marked.js | 20 ++++++++++---------- test/unit/marked-spec.js | 15 +++++++++------ 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index 82bd8f20ff..f1a9079b57 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -246,11 +246,14 @@ module.exports = class Lexer { cutSrc = src; if (this.options.extensions?.startBlock) { let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; this.options.extensions.startBlock.forEach(function(getStartIndex) { - startIndex = Math.max(0, Math.min(getStartIndex(src), startIndex)); + tempStart = getStartIndex(tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } }); - if (startIndex < Infinity && startIndex > 0) { - cutSrc = src.substring(0, startIndex); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { diff --git a/src/marked.js b/src/marked.js index 932c3afab7..5c9041758f 100644 --- a/src/marked.js +++ b/src/marked.js @@ -108,7 +108,7 @@ function marked(src, opt, callback) { try { const tokens = Lexer.lex(src, opt); if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens); + marked.walkTokens(tokens, opt.walkTokens, opt); } return Parser.parse(tokens, opt); } catch (e) { @@ -145,11 +145,9 @@ marked.use = function(extension) { if (!Array.isArray(extension)) { // Wrap in array if not already to unify processing extension = [extension]; } + const opts = merge({}, ...extension); - opts.tokenizer = null; - opts.renderer = null; - opts.extensions = null; - const extensions = { renderers: {}, walkableTokens: {} }; + const extensions = marked.defaults.extensions || { renderers: {}, walkableTokens: {} }; let hasExtensions; extension.forEach((pack) => { @@ -193,6 +191,7 @@ marked.use = function(extension) { const renderer = marked.defaults.renderer || new Renderer(); for (const prop in pack.renderer) { const prevRenderer = renderer[prop]; + // Replace renderer with func to run extension, but fall back if fail renderer[prop] = (...args) => { let ret = pack.renderer[prop].apply(renderer, args); if (ret === false) { @@ -207,6 +206,7 @@ marked.use = function(extension) { const tokenizer = marked.defaults.tokenizer || new Tokenizer(); for (const prop in pack.tokenizer) { const prevTokenizer = tokenizer[prop]; + // Replace tokenizer with func to run extension, but fall back if fail tokenizer[prop] = (...args) => { let ret = pack.tokenizer[prop].apply(tokenizer, args); if (ret === false) { @@ -228,13 +228,13 @@ marked.use = function(extension) { } }; } - }); - if (hasExtensions) { - opts.extensions = extensions; - } + if (hasExtensions) { + opts.extensions = extensions; + } - marked.setOptions(opts); + marked.setOptions(opts); + }); }; /** diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 48c5fc19f8..40a9066927 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -373,7 +373,7 @@ describe('use extension', () => { } }, renderer(token) { - return `<${token.type}>${token.text}\n`; + return `<${token.type}>${this.parseInline(token.tokens)}\n`; } }, { name: `inline-${name}`, @@ -398,16 +398,17 @@ describe('use extension', () => { return { type: 'heading', raw: `# ${name}`, - text: `used ${name}` + text: `used ${name}`, + depth: 1 }; } return false; } }, renderer: { - heading(text, level, raw, slugger) { + heading(text, depth, raw, slugger) { if (text === name) { - return `${text}\n`; + return `${text}\n`; } return false; } @@ -416,7 +417,8 @@ describe('use extension', () => { if (token.text === `used ${name}`) { token.text += ' walked'; } - } + }, + headerIds: false }; } @@ -434,7 +436,8 @@ describe('use extension', () => { # no extension `); - expect(`\n${html}\n`.replace(/\n+/, '\n')).toBe(` + + expect(`\n${html}\n`.replace(/\n+/g, '\n')).toBe(` used extension1 walked used extension2 walked

used extension1 walked From d8bbb74587e4feeb0cf141c8833893712d901e44 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 20 May 2021 15:11:56 -0400 Subject: [PATCH 34/48] Rebase --- lib/marked.esm.js | 78 ++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/lib/marked.esm.js b/lib/marked.esm.js index a3ec1ad3f1..17ac1c82e9 100644 --- a/lib/marked.esm.js +++ b/lib/marked.esm.js @@ -1474,20 +1474,19 @@ var Lexer_1 = class Lexer { if (this.options.pedantic) { src = src.replace(/^ +$/gm, ''); } - let token, i, l, lastToken, tokensParsed, cutSrc, lastParagraphClipped; + let token, i, l, lastToken, cutSrc, lastParagraphClipped; while (src) { - // extensions - if (this.options.extensions?.block) { - tokensParsed = false; - this.options.extensions.block.forEach((extTokenizer) => { + if (this.options?.extensions?.block + && this.options.extensions.block.some((extTokenizer) => { if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); - tokensParsed = true; + return true; } - }); - if (tokensParsed) { continue; } + return false; + })) { + continue; } // newline @@ -1598,11 +1597,14 @@ var Lexer_1 = class Lexer { cutSrc = src; if (this.options.extensions?.startBlock) { let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; this.options.extensions.startBlock.forEach(function(getStartIndex) { - startIndex = Math.max(0, Math.min(getStartIndex(src), startIndex)); + tempStart = getStartIndex(tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } }); - if (startIndex < Infinity && startIndex > 0) { - cutSrc = src.substring(0, startIndex); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); } } if (top && (token = this.tokenizer.paragraph(cutSrc))) { @@ -1716,7 +1718,7 @@ var Lexer_1 = class Lexer { // String with links masked to avoid interference with em and strong let maskedSrc = src; let match; - let keepPrevChar, prevChar, tokensParsed; + let keepPrevChar, prevChar; // Mask out reflinks if (this.tokens.links) { @@ -1746,16 +1748,16 @@ var Lexer_1 = class Lexer { keepPrevChar = false; // extensions - if (this.options.extensions?.inline) { - tokensParsed = false; - this.options.extensions.inline.forEach((extTokenizer) => { + if (this.options?.extensions?.inline + && this.options.extensions.inline.some((extTokenizer) => { if (token = extTokenizer.call(this, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); - tokensParsed = true; + return true; } - }); - if (tokensParsed) { continue; } + return false; + })) { + continue; } // escape @@ -2576,15 +2578,15 @@ marked.use = function(extension) { if (!Array.isArray(extension)) { // Wrap in array if not already to unify processing extension = [extension]; } + const opts = merge$2({}, ...extension); - opts.tokenizer = null; - opts.renderer = null; - opts.extensions = null; - const extensions = { renderers: {}, walkableTokens: {} }; + const extensions = marked.defaults.extensions || { renderers: {}, walkableTokens: {} }; + let hasExtensions; extension.forEach((pack) => { // ==-- Parse "addon" extensions --== // if (pack.extensions) { + hasExtensions = true; pack.extensions.forEach((ext) => { if (ext.renderer && ext.name) { // Renderers must have 'name' property extensions.renderers[ext.name] = ext.renderer; @@ -2622,6 +2624,7 @@ marked.use = function(extension) { const renderer = marked.defaults.renderer || new Renderer_1(); for (const prop in pack.renderer) { const prevRenderer = renderer[prop]; + // Replace renderer with func to run extension, but fall back if fail renderer[prop] = (...args) => { let ret = pack.renderer[prop].apply(renderer, args); if (ret === false) { @@ -2636,6 +2639,7 @@ marked.use = function(extension) { const tokenizer = marked.defaults.tokenizer || new Tokenizer_1(); for (const prop in pack.tokenizer) { const prevTokenizer = tokenizer[prop]; + // Replace tokenizer with func to run extension, but fall back if fail tokenizer[prop] = (...args) => { let ret = pack.tokenizer[prop].apply(tokenizer, args); if (ret === false) { @@ -2657,48 +2661,46 @@ marked.use = function(extension) { } }; } - }); - if (Object.keys(extensions).length) { - opts.extensions = extensions; - } + if (hasExtensions) { + opts.extensions = extensions; + } - marked.setOptions(opts); + marked.setOptions(opts); + }); }; /** * Run callback for every token */ -marked.walkTokens = function(tokens, callback, opt) { +marked.walkTokens = function(tokens, callback) { for (const token of tokens) { callback(token); switch (token.type) { case 'table': { for (const cell of token.tokens.header) { - marked.walkTokens(cell, callback, opt); + marked.walkTokens(cell, callback); } for (const row of token.tokens.cells) { for (const cell of row) { - marked.walkTokens(cell, callback, opt); + marked.walkTokens(cell, callback); } } break; } case 'list': { - marked.walkTokens(token.items, callback, opt); + marked.walkTokens(token.items, callback); break; } default: { - if (opt?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions - opt.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { - if (walkableTokens !== 'tokens') { - marked.walkTokens(token[walkableTokens], callback, opt); - } + if (marked.defaults?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions + marked.defaults?.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { + marked.walkTokens(token[walkableTokens], callback); }); } - if (token.tokens) { - marked.walkTokens(token.tokens, callback, opt); + if (token.tokens && !marked.defaults?.extensions?.walkableTokens[token.type]?.tokens) { + marked.walkTokens(token.tokens, callback); } } } From e6b4bbd1169288129e78fdfff70021702ebc1222 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 20 May 2021 19:25:59 -0400 Subject: [PATCH 35/48] Update src/marked.js Co-authored-by: Tony Brix --- src/marked.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marked.js b/src/marked.js index 5c9041758f..ee6ed7c5d9 100644 --- a/src/marked.js +++ b/src/marked.js @@ -191,7 +191,7 @@ marked.use = function(extension) { const renderer = marked.defaults.renderer || new Renderer(); for (const prop in pack.renderer) { const prevRenderer = renderer[prop]; - // Replace renderer with func to run extension, but fall back if fail + // Replace renderer with func to run extension, but fall back if false renderer[prop] = (...args) => { let ret = pack.renderer[prop].apply(renderer, args); if (ret === false) { From d9ec74df0c074956cb22c7ebc21ad36bb725c216 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 20 May 2021 19:47:25 -0400 Subject: [PATCH 36/48] Small fixes --- src/Lexer.js | 11 +++++++---- src/marked.js | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Lexer.js b/src/Lexer.js index f1a9079b57..03a06960f6 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -249,7 +249,7 @@ module.exports = class Lexer { const tempSrc = src.slice(1); let tempStart; this.options.extensions.startBlock.forEach(function(getStartIndex) { - tempStart = getStartIndex(tempSrc); + tempStart = getStartIndex.call(this, tempSrc); if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } }); if (startIndex < Infinity && startIndex >= 0) { @@ -509,11 +509,14 @@ module.exports = class Lexer { cutSrc = src; if (this.options.extensions?.startInline) { let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; this.options.extensions.startInline.forEach(function(getStartIndex) { - startIndex = Math.max(0, Math.min(getStartIndex(src), startIndex)); + tempStart = getStartIndex.call(this, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } }); - if (startIndex < Infinity && startIndex > 0) { - cutSrc = src.substring(0, startIndex); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); } } if (token = this.tokenizer.inlineText(cutSrc, inRawBlock, smartypants)) { diff --git a/src/marked.js b/src/marked.js index ee6ed7c5d9..9ab9c6ec84 100644 --- a/src/marked.js +++ b/src/marked.js @@ -108,7 +108,7 @@ function marked(src, opt, callback) { try { const tokens = Lexer.lex(src, opt); if (opt.walkTokens) { - marked.walkTokens(tokens, opt.walkTokens, opt); + marked.walkTokens(tokens, opt.walkTokens); } return Parser.parse(tokens, opt); } catch (e) { From caab77b461d6a378f0fbe688e28129cf7002473e Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 20 May 2021 20:29:59 -0400 Subject: [PATCH 37/48] Change walkableTokens to childTokens * Also only walk `token.tokens` by default if `childTokens` is not provided, or it is specifically listed in `childTokens` --- src/marked.js | 14 +++++++------- test/unit/marked-spec.js | 15 ++++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/marked.js b/src/marked.js index 9ab9c6ec84..1f8a8e9acc 100644 --- a/src/marked.js +++ b/src/marked.js @@ -147,7 +147,7 @@ marked.use = function(extension) { } const opts = merge({}, ...extension); - const extensions = marked.defaults.extensions || { renderers: {}, walkableTokens: {} }; + const extensions = marked.defaults.extensions || { renderers: {}, childTokens: {} }; let hasExtensions; extension.forEach((pack) => { @@ -158,8 +158,8 @@ marked.use = function(extension) { if (ext.renderer && ext.name) { // Renderers must have 'name' property extensions.renderers[ext.name] = ext.renderer; } - if (ext.walkableTokens && ext.name) { // walkableTokens must have 'name' - extensions.walkableTokens[ext.name] = ext.walkableTokens; + if (ext.childTokens && ext.name) { // childTokens must have 'name' + extensions.childTokens[ext.name] = ext.childTokens; } if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property if (extensions[ext.level]) { @@ -261,12 +261,12 @@ marked.walkTokens = function(tokens, callback) { break; } default: { - if (marked.defaults?.extensions?.walkableTokens?.[token.type]) { // Walk any extensions - marked.defaults?.extensions.walkableTokens[token.type].forEach(function(walkableTokens) { - marked.walkTokens(token[walkableTokens], callback); + if (marked.defaults?.extensions?.childTokens?.[token.type]) { // Walk any extensions + marked.defaults?.extensions.childTokens[token.type].forEach(function(childTokens) { + marked.walkTokens(token[childTokens], callback); }); } - if (token.tokens && !marked.defaults?.extensions?.walkableTokens[token.type]?.tokens) { + if (token.tokens && !marked.defaults?.extensions?.childTokens[token.type]) { marked.walkTokens(token.tokens, callback); } } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 40a9066927..d45e9b3182 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -318,7 +318,7 @@ describe('use extension', () => { expect(html).toBe(''); }); - it('should handle list of walkable tokens', () => { + it('should walk only specified child tokens', () => { const walkableDescription = { extensions: [{ name: 'walkableDescription', @@ -332,26 +332,27 @@ describe('use extension', () => { type: 'walkableDescription', raw: match[0], // This is the text that you want your token to consume from the source dt: this.inlineTokens(match[1].trim()), // You can add additional properties to your tokens to pass along to the renderer - dd: this.inlineTokens(match[2].trim()) + dd: this.inlineTokens(match[2].trim()), + tokens: this.inlineTokens('unwalked') }; } }, renderer(token) { - return `\n

${this.parseInline(token.dt)}
${this.parseInline(token.dd)}
`; + return `\n
${this.parseInline(token.dt)} - ${this.parseInline(token.tokens)}
${this.parseInline(token.dd)}
`; }, - walkableTokens: ['dd', 'dt'] + childTokens: ['dd', 'dt'] }], walkTokens(token) { if (token.type === 'text') { - token.text += 'A'; + token.text += ' walked'; } } }; marked.use(walkableDescription); const html = marked(': Topic 1 : Description 1\n' + ': **Topic 2** : *Description 2*'); - expect(html).toBe('

\n

Topic 1A
Description 1A
' - + '\n
Topic 2A
Description 2A

\n'); + expect(html).toBe('

\n

Topic 1 walked - unwalked
Description 1 walked
' + + '\n
Topic 2 walked - unwalked
Description 2 walked

\n'); }); describe('multiple extensions', () => { From ac350309f7105af795f3266cea47c5a0d23e8e09 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 20 May 2021 23:39:42 -0400 Subject: [PATCH 38/48] Unit test that shows working "style tag" type syntax --- test/unit/marked-spec.js | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index d45e9b3182..91506f2ce7 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -466,6 +466,56 @@ used extension2 walked

}); }); + it('should allow deleting/editing tokens', () => { + const styleTags = { + extensions: [{ + name: 'inlineStyleTag', + level: 'inline', + start(src) { return src.match(/ *{[^\{]/)?.index; }, + tokenizer(src, tokens) { + const rule = /^ *{([^\{\}\n]+)}$/; + const match = rule.exec(src); + if (match) { + return { + type: 'inlineStyleTag', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1] + }; + } + } + }, + { + name: 'styled', + renderer(token) { + token.type = token.originalType; + const text = this.parse([token]); + const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); + if (openingTag) { + return `${openingTag[1]} ${token.style}${openingTag[2]}`; + } + return text; + } + }], + walkTokens(token) { + if (token.tokens) { + const finalChildToken = token.tokens[token.tokens.length - 1]; + if (finalChildToken?.type === 'inlineStyleTag') { + token.originalType = token.type; + token.type = 'styled'; + token.style = `style="color:${finalChildToken.text};"`; + token.tokens.pop(); + } + } + }, + headerIds: false + }; + marked.use(styleTags); + const html = marked('This is a *paragraph* with blue text. {blue}\n' + + '# This is a *header* with red text {red}'); + expect(html).toBe('

This is a paragraph with blue text.

\n' + + '

This is a header with red text

\n'); + }); + it('should use renderer', () => { const extension = { renderer: { From eb0e7903bf7deaa7cfd1917d1642fb21393920a3 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 21 May 2021 23:52:34 -0400 Subject: [PATCH 39/48] Last loaded extension gets priority, but will fall back to next extension with same name if returns false --- src/marked.js | 22 +++++++++++++++++----- test/unit/marked-spec.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/marked.js b/src/marked.js index 1f8a8e9acc..771160b20a 100644 --- a/src/marked.js +++ b/src/marked.js @@ -156,14 +156,23 @@ marked.use = function(extension) { hasExtensions = true; pack.extensions.forEach((ext) => { if (ext.renderer && ext.name) { // Renderers must have 'name' property - extensions.renderers[ext.name] = ext.renderer; - } - if (ext.childTokens && ext.name) { // childTokens must have 'name' - extensions.childTokens[ext.name] = ext.childTokens; + const prevRenderer = extensions.renderers?.[ext.name] || null; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if fail + extensions.renderers[ext.name] = function(...args) { + let ret = ext.renderer.apply(this, args);// (args); + if (ret === false) { + ret = prevRenderer.apply(this, args);// (args); + } + return ret; + }; + } else { + extensions.renderers[ext.name] = ext.renderer; + } } if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property if (extensions[ext.level]) { - extensions[ext.level].push(ext.tokenizer); + extensions[ext.level].unshift(ext.tokenizer); } else { extensions[ext.level] = [ext.tokenizer]; } @@ -183,6 +192,9 @@ marked.use = function(extension) { } } } + if (ext.childTokens && ext.name) { // childTokens must have 'name' + extensions.childTokens[ext.name] = ext.childTokens; + } }); } diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 91506f2ce7..fff710a9a2 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -423,6 +423,33 @@ describe('use extension', () => { }; } + function createFalseExtension(name) { + return { + extensions: [{ + name: `block-${name}`, + level: 'block', + start(src) { return src.indexOf('::'); }, + tokenizer(src, tokens) { + return false; + }, + renderer(token) { + return false; + } + }, { + name: `inline-${name}`, + level: 'inline', + start(src) { return src.indexOf(':'); }, + tokenizer(src, tokens) { + return false; + }, + renderer(token) { + return false; + } + }], + headerIds: false + }; + } + function runTest() { const html = marked(` ::extension1 @@ -464,6 +491,17 @@ used extension2 walked

runTest(); }); + + it('should fall back to any extensions with the same name if the first returns false', () => { + marked.use([ + createExtension('extension1'), + createExtension('extension2'), + createFalseExtension('extension1'), + createFalseExtension('extension2') + ]); + + runTest(); + }); }); it('should allow deleting/editing tokens', () => { From ccb9869d9e278ee886b79036f2630746837fb5ad Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 22 May 2021 13:43:40 -0400 Subject: [PATCH 40/48] Cleanup --- src/marked.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/marked.js b/src/marked.js index 771160b20a..d9262bfd10 100644 --- a/src/marked.js +++ b/src/marked.js @@ -156,13 +156,13 @@ marked.use = function(extension) { hasExtensions = true; pack.extensions.forEach((ext) => { if (ext.renderer && ext.name) { // Renderers must have 'name' property - const prevRenderer = extensions.renderers?.[ext.name] || null; + const prevRenderer = extensions.renderers?.[ext.name]; if (prevRenderer) { - // Replace extension with func to run new extension but fall back if fail + // Replace extension with func to run new extension but fall back if false extensions.renderers[ext.name] = function(...args) { - let ret = ext.renderer.apply(this, args);// (args); + let ret = ext.renderer.apply(this, args); if (ret === false) { - ret = prevRenderer.apply(this, args);// (args); + ret = prevRenderer.apply(this, args); } return ret; }; @@ -218,7 +218,7 @@ marked.use = function(extension) { const tokenizer = marked.defaults.tokenizer || new Tokenizer(); for (const prop in pack.tokenizer) { const prevTokenizer = tokenizer[prop]; - // Replace tokenizer with func to run extension, but fall back if fail + // Replace tokenizer with func to run extension, but fall back if false tokenizer[prop] = (...args) => { let ret = pack.tokenizer[prop].apply(tokenizer, args); if (ret === false) { @@ -234,7 +234,7 @@ marked.use = function(extension) { if (pack.walkTokens) { const walkTokens = marked.defaults.walkTokens; opts.walkTokens = (token) => { - pack.walkTokens(token); + pack.walkTokens.call(this, token); if (walkTokens) { walkTokens(token); } From bbc8685e9786073ce393e79d9de4b5df5a8cb679 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 22 May 2021 14:12:50 -0400 Subject: [PATCH 41/48] Errors for missing name or invalid 'level' --- src/marked.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/marked.js b/src/marked.js index d9262bfd10..513789e088 100644 --- a/src/marked.js +++ b/src/marked.js @@ -155,7 +155,10 @@ marked.use = function(extension) { if (pack.extensions) { hasExtensions = true; pack.extensions.forEach((ext) => { - if (ext.renderer && ext.name) { // Renderers must have 'name' property + if (!ext.name) { + throw new Error('extension name required'); + } + if (ext.renderer) { // Renderer extensions const prevRenderer = extensions.renderers?.[ext.name]; if (prevRenderer) { // Replace extension with func to run new extension but fall back if false @@ -170,7 +173,10 @@ marked.use = function(extension) { extensions.renderers[ext.name] = ext.renderer; } } - if (ext.tokenizer && ext.level) { // Tokenizers must have 'level' property + if (ext.tokenizer) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } if (extensions[ext.level]) { extensions[ext.level].unshift(ext.tokenizer); } else { @@ -192,7 +198,7 @@ marked.use = function(extension) { } } } - if (ext.childTokens && ext.name) { // childTokens must have 'name' + if (ext.childTokens) { // Child tokens to be visited by walkTokens extensions.childTokens[ext.name] = ext.childTokens; } }); From ecc47d418f9d0df5ace76097ceb729166e48e19e Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 22 May 2021 19:49:43 -0400 Subject: [PATCH 42/48] override original tokenizer/renderer with same name, but fall back if returns false --- src/Parser.js | 38 ++++++++++++++++++++++++-------------- test/unit/marked-spec.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/Parser.js b/src/Parser.js index a09c3ace36..f766269e0b 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -57,11 +57,22 @@ module.exports = class Parser { item, checked, task, - checkbox; + checkbox, + ret; const l = tokens.length; for (i = 0; i < l; i++) { token = tokens[i]; + + // Run any renderer extensions + if (this.options.extensions?.renderers?.[token.type]) { + ret = this.options.extensions.renderers[token.type].call(this, token); + if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'paragraph', 'text'].includes(token.type)) { + out += ret || ''; + continue; + } + } + switch (token.type) { case 'space': { continue; @@ -181,12 +192,6 @@ module.exports = class Parser { } default: { - // Run any renderer extensions - if (this.options.extensions?.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type].call(this, token) || ''; - continue; - } - const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { console.error(errMsg); @@ -208,11 +213,22 @@ module.exports = class Parser { renderer = renderer || this.renderer; let out = '', i, - token; + token, + ret; const l = tokens.length; for (i = 0; i < l; i++) { token = tokens[i]; + + // Run any renderer extensions + if (this.options.extensions?.renderers?.[token.type]) { + ret = this.options.extensions.renderers[token.type].call(this, token); + if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(token.type)) { + out += ret || ''; + continue; + } + } + switch (token.type) { case 'escape': { out += renderer.text(token.text); @@ -255,12 +271,6 @@ module.exports = class Parser { break; } default: { - // Run any renderer extensions - if (this.options.extensions?.renderers?.[token.type]) { - out += this.options.extensions.renderers[token.type].call(this, token) || ''; - continue; - } - const errMsg = 'Token with "' + token.type + '" type was not found.'; if (this.options.silent) { console.error(errMsg); diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index fff710a9a2..bb39a90205 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -318,6 +318,42 @@ describe('use extension', () => { expect(html).toBe(''); }); + it('should override original tokenizer/renderer with same name, but fall back if returns false', () => { + const extension = { + extensions: [{ + name: 'heading', + level: 'block', + tokenizer(src) { + return false; // fall back to default `heading` tokenizer + }, + renderer(token) { + return '' + token.text + ' RENDERER EXTENSION\n'; + } + }, + { + name: 'code', + level: 'block', + tokenizer(src) { + const rule = /^:([^\n]*):(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'code', + raw: match[0], + text: match[1].trim() + ' TOKENIZER EXTENSION' + }; + } + }, + renderer(token) { + return false; // fall back to default `code` renderer + } + }] + }; + marked.use(extension); + const html = marked('# extension1\n:extension2:'); + expect(html).toBe('

extension1 RENDERER EXTENSION

\n
extension2 TOKENIZER EXTENSION\n
\n'); + }); + it('should walk only specified child tokens', () => { const walkableDescription = { extensions: [{ From c175894f9c1018d82544ca1232dc30fc5ffa9e01 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Sun, 30 May 2021 23:35:28 -0500 Subject: [PATCH 43/48] test fallback if returning false in extension --- test/unit/marked-spec.js | 62 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index bb39a90205..0ca7a70beb 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -310,12 +310,68 @@ describe('use extension', () => { } }, renderer(token) { + if (token.text === 'test') { + return 'test'; + } + return false; + } + }; + const fallbackRenderer = { + name: 'test', + level: 'block', + renderer(token) { + if (token.text === 'Test') { + return 'fallback'; + } + return false; + } + }; + marked.use({ extensions: [fallbackRenderer, extension] }); + const html = marked(':Test:\n\n:test:\n\n:none:'); + expect(html).toBe('fallbacktest'); + }); + + it('should fall back when tokenizers return false', () => { + const extension = { + name: 'test', + level: 'block', + tokenizer(src) { + const rule = /^:([^\n]*):(?:\n|$)/; + const match = rule.exec(src); + if (match) { + return { + type: 'test', + raw: match[0], // This is the text that you want your token to consume from the source + text: match[1].trim() // You can add additional properties to your tokens to pass along to the renderer + }; + } + return false; + }, + renderer(token) { + return token.text; + } + }; + const extension2 = { + name: 'test', + level: 'block', + tokenizer(src) { + const rule = /^:([^\n]*):(?:\n|$)/; + const match = rule.exec(src); + if (match) { + if (match[1].match(/^[A-Z]/)) { + return { + type: 'test', + raw: match[0], + text: match[1].trim().toUpperCase() + }; + } + } return false; } }; - marked.use({ extensions: [extension] }); - const html = marked(':Test:'); - expect(html).toBe(''); + marked.use({ extensions: [extension, extension2] }); + const html = marked(':Test:\n\n:test:'); + expect(html).toBe('TESTtest'); }); it('should override original tokenizer/renderer with same name, but fall back if returns false', () => { From 697315a23fb5444f67b8da1d1ac73e47b9340399 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 31 May 2021 23:09:16 -0400 Subject: [PATCH 44/48] Update Docs, first pass --- docs/USING_PRO.md | 333 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 268 insertions(+), 65 deletions(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index 0119911ff4..fb40c7a779 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -1,24 +1,71 @@ ## Extending Marked -To champion the single-responsibility and open/closed principles, we have tried to make it relatively painless to extend marked. If you are looking to add custom functionality, this is the place to start. +To champion the single-responsibility and open/closed principles, we have tried to make it relatively painless to extend Marked. If you are looking to add custom functionality, this is the place to start.

marked.use()

-`marked.use(options)` is the recommended way to extend marked. The options object can contain any [option](/using_advanced#options) available in marked. +`marked.use(extension)` is the recommended way to extend Marked. The `extension` object can contain any [option](/using_advanced#options) available in Marked: -The `renderer` and `tokenizer` options can be an object with functions that will be merged into the `renderer` and `tokenizer` respectively. -The `renderer` and `tokenizer` functions can return false to fallback to the previous function. +```js +const marked = require('marked'); + +marked.use({ + pedantic: false, + gfm: true, + breaks: false, + sanitize: false, + smartLists: true, + smartypants: false, + xhtml: false +}); +``` + +You can also supply an array of multiple `extension` objects. + +``` +marked.use([myExtension, extension2, extension3]); + +\\ EQUIVALENT TO: + +marked.use(myExtension); +marked.use(extension2); +marked.use(extension3); + +``` -The `walkTokens` option can be a function that will be called with every token before rendering. When calling `use` multiple times with different `walkTokens` functions each function will be called in the **reverse** order in which they were assigned. +All options will overwrite those previously set, except for the following options which will be merged with the existing framework and can be used to change or extend the functionality of Marked: `renderer`, `tokenizer`, `walkTokens`, and `extensions`. + +* The `renderer` and `tokenizer` options are objects with functions that will be merged into the built-in `renderer` and `tokenizer` respectively. + +* The `walkTokens` option is a function that will be called to post-process every token before rendering. + +* The `extensions` option is an array of objects that can contain additional custom `renderer` and `tokenizer` steps that will execute before any of the default parsing logic occurs. + +*** + +

The Marked Pipeline

+ +Before building your custom extensions, it is important to understand the components that Marked uses to translate from Markdown to HTML: + +1) The user supplies Marked with an input string to be translated. +2) The `lexer` feeds segments of the input text string into each `tokenizer`, and from their output, generates a series of tokens in a nested tree structure. +3) Each `tokenizer` receives a segment of Markdown text and, if it matches a particular pattern, generates a token object containing any relevant information. +4) The `walkTokens` function will traverse every token in the tree and perform any final adjustments to the token contents. +4) The `parser` traverses the token tree and feeds each token into the appropriate `renderer`, and concatenates their outputs into the final HTML result. +5) Each `renderer` receives a token and manipulates its contents to generate a segment of HTML. + +Marked provides methods of directly overriding the `renderer` and `tokenizer` for any existing token type, as well as inserting additional custom `renderer` and `tokenizer` functions to handle entirely custom syntax. + +*** -All other options will overwrite previously set options. +

The Renderer : renderer

-

The renderer

+The renderer defines the HTML output of a given token. If you supply a `renderer` object to the Marked options, it will be merged with the built-in renderer and any functions inside will override the default handling of that token type. -The renderer defines the output of the parser. +Calling `marked.use()` to override the same function multiple times will give priority to the version that was assigned *last*. Overriding functions can return `false` to fall back to the previous override in the sequence, or resume default behavior if all overrides return `false`. Returning any other value (including nothing) will prevent fallback behavior. -**Example:** Overriding default heading token by adding an embedded anchor tag like on GitHub. +**Example:** Overriding output of the default `heading` token by adding an embedded anchor tag like on GitHub. ```js // Create reference instance @@ -56,20 +103,31 @@ console.log(marked('# heading+')); ``` -### Block level renderer methods - -- code(*string* code, *string* infostring, *boolean* escaped) -- blockquote(*string* quote) -- html(*string* html) -- heading(*string* text, *number* level, *string* raw, *Slugger* slugger) -- hr() -- list(*string* body, *boolean* ordered, *number* start) -- listitem(*string* text, *boolean* task, *boolean* checked) -- checkbox(*boolean* checked) -- paragraph(*string* text) -- table(*string* header, *string* body) -- tablerow(*string* content) -- tablecell(*string* content, *object* flags) +### Block-level renderer methods + +- **code**(*string* code, *string* infostring, *boolean* escaped) +- **blockquote**(*string* quote) +- **html**(*string* html) +- **heading**(*string* text, *number* level, *string* raw, *Slugger* slugger) +- **hr**() +- **list**(*string* body, *boolean* ordered, *number* start) +- **listitem**(*string* text, *boolean* task, *boolean* checked) +- **checkbox**(*boolean* checked) +- **paragraph**(*string* text) +- **table**(*string* header, *string* body) +- **tablerow**(*string* content) +- **tablecell**(*string* content, *object* flags) + +### Inline-level renderer methods + +- **strong**(*string* text) +- **em**(*string* text) +- **codespan**(*string* code) +- **br**() +- **del**(*string* text) +- **link**(*string* href, *string* title, *string* text) +- **image**(*string* href, *string* title, *string* text) +- **text**(*string* text) `slugger` has the `slug` method to create a unique id from value: @@ -103,20 +161,13 @@ slugger.slug('foo') // foo-4 } ``` -### Inline level renderer methods +*** -- strong(*string* text) -- em(*string* text) -- codespan(*string* code) -- br() -- del(*string* text) -- link(*string* href, *string* title, *string* text) -- image(*string* href, *string* title, *string* text) -- text(*string* text) +

The Tokenizer : tokenizer

-

The tokenizer

+The tokenizer defines how to turn markdown text into tokens. If you supply a `tokenizer` object to the Marked options, it will be merged with the built-in tokenizer and any functions inside will override the default handling of that token type. -The tokenizer defines how to turn markdown text into tokens. +Calling `marked.use()` to override the same function multiple times will give priority to the version that was assigned *last*. Overriding functions can return `false` to fall back to the previous override in the sequence, or resume default behavior if all overrides return `false`. Returning any other value (including nothing) will prevent fallback behavior. **Example:** Overriding default `codespan` tokenizer to include LaTeX. @@ -157,34 +208,34 @@ console.log(marked('$ latex code $\n\n` other code `')); ### Block level tokenizer methods -- space(*string* src) -- code(*string* src) -- fences(*string* src) -- heading(*string* src) -- nptable(*string* src) -- hr(*string* src) -- blockquote(*string* src) -- list(*string* src) -- html(*string* src) -- def(*string* src) -- table(*string* src) -- lheading(*string* src) -- paragraph(*string* src) -- text(*string* src) +- **space**(*string* src) +- **code**(*string* src) +- **fences**(*string* src) +- **heading**(*string* src) +- **nptable**(*string* src) +- **hr**(*string* src) +- **blockquote**(*string* src) +- **list**(*string* src) +- **html**(*string* src) +- **def**(*string* src) +- **table**(*string* src) +- **lheading**(*string* src) +- **paragraph**(*string* src) +- **text**(*string* src) ### Inline level tokenizer methods -- escape(*string* src) -- tag(*string* src, *bool* inLink, *bool* inRawBlock) -- link(*string* src) -- reflink(*string* src, *object* links) -- emStrong(*string* src, *string* maskedSrc, *string* prevChar) -- codespan(*string* src) -- br(*string* src) -- del(*string* src) -- autolink(*string* src, *function* mangle) -- url(*string* src, *function* mangle) -- inlineText(*string* src, *bool* inRawBlock, *function* smartypants) +- **escape**(*string* src) +- **tag**(*string* src, *bool* inLink, *bool* inRawBlock) +- **link**(*string* src) +- **reflink**(*string* src, *object* links) +- **emStrong**(*string* src, *string* maskedSrc, *string* prevChar) +- **codespan**(*string* src) +- **br**(*string* src) +- **del**(*string* src) +- **autolink**(*string* src, *function* mangle) +- **url**(*string* src, *function* mangle) +- **inlineText**(*string* src, *bool* inRawBlock, *function* smartypants) `mangle` is a method that changes text to HTML character references: @@ -202,10 +253,14 @@ smartypants('"this ... string"') // "“this … string”" ``` -

Walk Tokens

+*** + +

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. The return value of the function is ignored. +`marked.use()` can be called multiple times with different `walkTokens` functions. Each function will be called in order, starting with the **last** function that was assigned. + **Example:** Overriding heading tokens to start at h2. ```js @@ -231,17 +286,165 @@ console.log(marked('# heading 2\n\n## heading 3'));

heading 3

``` -

The lexer

+*** -The lexer takes a markdown string and calls the tokenizer functions. +

Custom Extensions : extensions

-

The parser

+You may supply an `extensions` array to the `options` object. This array can contain any number of `extension` objects, using the following properties: -The parser takes tokens as input and calls the renderer functions. +
+
name
+
A string used to identify the token that will be handled by this extension. + +If the name matches an existing extension name, or an existing method in the tokenizer/renderer methods listed above, they will override the previously assigned behavior, with priority on the extension that was assigned **last**. An extension can return `false` to fall back to the previous behavior.
+ +
level
+
A string to determine when to run the extension tokenizer. Must be equal to 'block' or 'inline'. + +A **block-level** extension will be handled before any of the block-level tokenizer methods listed above, and generally consists of 'container-type' text (paragraphs, tables, blockquotes, etc.). + +An **inline-level** extension will be handled inside each block-level token, before any of the inline-level tokenizer methods listed above. These generally consist of 'style-type' text (italics, bold, etc.).
+ +
start(string src)
+
A function that returns the index of the next potential start of the custom token. + +The index can be the result of a src.match().index, or even a simple src.index(). Marked will use this function to ensure that it does not skip over any text that should be part of the custom token.
+ +
tokenizer(string src, array tokens)
+
A function that reads a string of Markdown text and returns a generated token. The tokens parameter contains the array of tokens that have been generated by the lexer up to that point, and can be used to access the previous token, for instance. + +The return value should be an object with the following parameters: + +
+
type
+
A string that matches the name parameter of the extension.
+ +
raw
+
A string containing all of the text that this token consumes from the source.
+ +
tokens [optional]
+
An array of child tokens that will be traversed by the walkTokens function by default.
+
+ +The returned token can also contain any other custom parameters of your choice that your custom `renderer` might need to access. + +The tokenizer function has access to the lexer in the `this` object, which can be used if any internal section of the string needs to be parsed further, such as in handling any inline syntax on the text within a block token. The key functions that may be useful include: + +
+
this.blockTokens(string text)
+
Runs the block tokenizer functions (including any extensions) on the provided text, and returns an array containing a nested tree of tokens.
+ +
this.inlineTokens(string text)
+
Runs the inline tokenizer functions (including any extensions) on the provided text, and returns an array containing a nested tree of tokens. This can be used to generate the tokens parameter.
+
+ +
renderer(object token)
+
A function that reads a token and returns the generated HTML output string. + +The renderer function has access to the parser in the `this` object, which can be used if any part of the token needs needs to be parsed further, such as any child tokens. The key functions that may be useful include: + +
+
this.parse(array tokens)
+
Runs the block renderer functions (including any extensions) on the provided array of tokens, and returns the resulting HTML string output.
+ +
this.parseInline(array tokens)
+
Runs the inline renderer functions (including any extensions) on the provided array of tokens, and returns the resulting HTML string output. This could be used to generate text from any child tokens, for example.
+
+ +
+ +
childTokens [optional]
+
An array of strings that match the names of any token parameters that should be traversed by the walkTokens functions. For instance, if you want to use a second custom parameter to contain child tokens in addition to tokens, it could be listed here. If childTokens is provided, the tokens array will not be walked by default unless it is also included in the childTokens array. +
+ + + + +**Example:** Add a custom syntax to generate `
` description lists. + +``` +const descriptionlist = { + name: 'descriptionList', + level: 'block', // Is this a block-level or inline-level tokenizer? + start(src) { return src.match(/:[^:\n]/)?.index; }, // Hint to Marked.js to stop and check for a match + tokenizer(src, tokens) { + const rule = /^(?::[^:\n]+:[^:\n]*(?:\n|$))+/; // Regex for the complete token + const match = rule.exec(src); + if (match) { + return { // Token to generate + type: 'descriptionList', // Should match "name" above + raw: match[0], // Text to consume from the source + text: match[0].trim(), // Additional custom properties + tokens: this.inlineTokens(match[0].trim()) // inlineTokens to process **bold**, *italics*, etc. + }; + } + }, + renderer(token) { + return `
${this.parseInline(token.tokens)}\n
`; // parseInline to turn child tokens into HTML + } +}; + +const description = { + name: 'description', + level: 'inline', // Is this a block-level or inline-level tokenizer? + start(src) { return src.match(/:/)?.index; }, // Hint to Marked.js to stop and check for a match + tokenizer(src, tokens) { + const rule = /^:([^:\n]+):([^:\n]*)(?:\n|$)/; // Regex for the complete token + const match = rule.exec(src); + if (match) { + return { // Token to generate + type: 'description', // Should match "name" above + raw: match[0], // Text to consume from the source + dt: this.inlineTokens(match[1].trim()), // Additional custom properties + dd: this.inlineTokens(match[2].trim()) + }; + } + }, + renderer(token) { + return `\n
${this.parseInline(token.dt)}
${this.parseInline(token.dd)}
`; + }, + childTokens: ['dt', 'dd'], // Any child tokens to be visited by walkTokens + walkTokens(token) { // Post-processing on the completed token tree + if (token.type === 'strong') { + token.text += ' walked'; + } + } +}; + +marked.use({ extensions: [descriptionlist, description] }); + +\\ EQUIVALENT TO: + +marked.use({extensions: [descriptionList] }); +marked.use({extensions: [description] }); + +console.log(marked('A Description List:\n' + + ': Topic 1 : Description 1\n' + + ': **Topic 2** : *Description 2*')); +``` + +**Output** + +``` +

A Description List:

+
+
Topic 1
Description 1
+
Topic 2 walked
Description 2
+
+``` *** -

Access to lexer and parser

+

The Lexer

+ +The lexer takes a markdown string and calls the tokenizer functions. + + +

The Parser

+ +The parser takes tokens as input and calls the renderer functions. + +

Access to Lexer and Parser

You also have direct access to the lexer and parser if you so desire. From dc828a0c5ecab2dbb12ae9d64cf017f12f1785e9 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 31 May 2021 23:46:33 -0400 Subject: [PATCH 45/48] Formatting changes to docs --- docs/USING_PRO.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md index fb40c7a779..605f9d4d82 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -259,7 +259,7 @@ smartypants('"this ... string"') 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. The return value of the function is ignored. -`marked.use()` can be called multiple times with different `walkTokens` functions. Each function will be called in order, starting with the **last** function that was assigned. +`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*. **Example:** Overriding heading tokens to start at h2. @@ -293,36 +293,36 @@ console.log(marked('# heading 2\n\n## heading 3')); You may supply an `extensions` array to the `options` object. This array can contain any number of `extension` objects, using the following properties:
-
name
+
name
A string used to identify the token that will be handled by this extension. If the name matches an existing extension name, or an existing method in the tokenizer/renderer methods listed above, they will override the previously assigned behavior, with priority on the extension that was assigned **last**. An extension can return `false` to fall back to the previous behavior.
-
level
+
level
A string to determine when to run the extension tokenizer. Must be equal to 'block' or 'inline'. A **block-level** extension will be handled before any of the block-level tokenizer methods listed above, and generally consists of 'container-type' text (paragraphs, tables, blockquotes, etc.). An **inline-level** extension will be handled inside each block-level token, before any of the inline-level tokenizer methods listed above. These generally consist of 'style-type' text (italics, bold, etc.).
-
start(string src)
+
start(string src)
A function that returns the index of the next potential start of the custom token. The index can be the result of a src.match().index, or even a simple src.index(). Marked will use this function to ensure that it does not skip over any text that should be part of the custom token.
-
tokenizer(string src, array tokens)
+
tokenizer(string src, array tokens)
A function that reads a string of Markdown text and returns a generated token. The tokens parameter contains the array of tokens that have been generated by the lexer up to that point, and can be used to access the previous token, for instance. The return value should be an object with the following parameters:
-
type
+
type
A string that matches the name parameter of the extension.
-
raw
+
raw
A string containing all of the text that this token consumes from the source.
-
tokens [optional]
+
tokens [optional]
An array of child tokens that will be traversed by the walkTokens function by default.
@@ -331,29 +331,29 @@ The returned token can also contain any other custom parameters of your choice t The tokenizer function has access to the lexer in the `this` object, which can be used if any internal section of the string needs to be parsed further, such as in handling any inline syntax on the text within a block token. The key functions that may be useful include:
-
this.blockTokens(string text)
+
this.blockTokens(string text)
Runs the block tokenizer functions (including any extensions) on the provided text, and returns an array containing a nested tree of tokens.
-
this.inlineTokens(string text)
+
this.inlineTokens(string text)
Runs the inline tokenizer functions (including any extensions) on the provided text, and returns an array containing a nested tree of tokens. This can be used to generate the tokens parameter.
-
renderer(object token)
+
renderer(object token)
A function that reads a token and returns the generated HTML output string. The renderer function has access to the parser in the `this` object, which can be used if any part of the token needs needs to be parsed further, such as any child tokens. The key functions that may be useful include:
-
this.parse(array tokens)
+
this.parse(array tokens)
Runs the block renderer functions (including any extensions) on the provided array of tokens, and returns the resulting HTML string output.
-
this.parseInline(array tokens)
+
this.parseInline(array tokens)
Runs the inline renderer functions (including any extensions) on the provided array of tokens, and returns the resulting HTML string output. This could be used to generate text from any child tokens, for example.
-
childTokens [optional]
+
childTokens [optional]
An array of strings that match the names of any token parameters that should be traversed by the walkTokens functions. For instance, if you want to use a second custom parameter to contain child tokens in addition to tokens, it could be listed here. If childTokens is provided, the tokens array will not be walked by default unless it is also included in the childTokens array.
@@ -362,7 +362,7 @@ The renderer function has access to the parser in the `this` object, which can b **Example:** Add a custom syntax to generate `
` description lists. -``` +``` js const descriptionlist = { name: 'descriptionList', level: 'block', // Is this a block-level or inline-level tokenizer? @@ -425,7 +425,7 @@ console.log(marked('A Description List:\n' **Output** -``` +``` bash

A Description List:

Topic 1
Description 1
From 00a91cc27cfcb1d3c5b7ad7f0fa51d7f357c7f93 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 1 Jun 2021 00:16:17 -0400 Subject: [PATCH 46/48] Update docs/USING_PRO.md Co-authored-by: Tony Brix --- 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 605f9d4d82..c93c003b0f 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -105,7 +105,7 @@ console.log(marked('# heading+')); ### Block-level renderer methods -- **code**(*string* code, *string* infostring, *boolean* escaped) +- **code**(*string* code, *string* infostring, *boolean* escaped) - **blockquote**(*string* quote) - **html**(*string* html) - **heading**(*string* text, *number* level, *string* raw, *Slugger* slugger) From 9eb1aa8eca08f9845868859933bd7f6f8b83464d Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 1 Jun 2021 21:59:09 -0400 Subject: [PATCH 47/48] Change marked.use to accept multiple parameters instead of an array --- src/marked.js | 10 +++------- test/unit/marked-spec.js | 8 ++++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/marked.js b/src/marked.js index 513789e088..540eb20852 100644 --- a/src/marked.js +++ b/src/marked.js @@ -141,16 +141,12 @@ marked.defaults = defaults; * Use Extension */ -marked.use = function(extension) { - if (!Array.isArray(extension)) { // Wrap in array if not already to unify processing - extension = [extension]; - } - - const opts = merge({}, ...extension); +marked.use = function(...args) { + const opts = merge({}, ...args); const extensions = marked.defaults.extensions || { renderers: {}, childTokens: {} }; let hasExtensions; - extension.forEach((pack) => { + args.forEach((pack) => { // ==-- Parse "addon" extensions --== // if (pack.extensions) { hasExtensions = true; diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js index 0ca7a70beb..0845909222 100644 --- a/test/unit/marked-spec.js +++ b/test/unit/marked-spec.js @@ -576,21 +576,21 @@ used extension2 walked

}); it('should merge extensions when calling marked.use with multiple extensions', () => { - marked.use([ + marked.use( createExtension('extension1'), createExtension('extension2') - ]); + ); runTest(); }); it('should fall back to any extensions with the same name if the first returns false', () => { - marked.use([ + marked.use( createExtension('extension1'), createExtension('extension2'), createFalseExtension('extension1'), createFalseExtension('extension2') - ]); + ); runTest(); }); From 0664ddd0ec0d057282df247ab8c6ac40de236f5f Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 1 Jun 2021 22:03:08 -0400 Subject: [PATCH 48/48] update docs --- 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 c93c003b0f..012f7634e8 100644 --- a/docs/USING_PRO.md +++ b/docs/USING_PRO.md @@ -21,10 +21,10 @@ marked.use({ }); ``` -You can also supply an array of multiple `extension` objects. +You can also supply multiple `extension` objects at once. ``` -marked.use([myExtension, extension2, extension3]); +marked.use(myExtension, extension2, extension3); \\ EQUIVALENT TO: