From 5e1b269fa5da4b78dbfa4b9fb224575c66e6ad21 Mon Sep 17 00:00:00 2001 From: David Anson Date: Fri, 11 Oct 2024 22:18:51 -0700 Subject: [PATCH] Refactor to freeze parser tokens only when custom rules are present (negligable performance improvement). --- demo/markdownlint-browser.js | 101 +++++++++++++++++++---------------- helpers/micromark-parse.cjs | 93 +++++++++++++++++--------------- lib/markdownlint.js | 6 ++- lib/md034.js | 2 +- 4 files changed, 110 insertions(+), 92 deletions(-) diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index e632eb40..8e34d1fd 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -986,26 +986,36 @@ const { isHtmlFlowComment } = __webpack_require__(/*! ./micromark-helpers.cjs */ const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = __webpack_require__(/*! ./shared.js */ "../helpers/shared.js"); /** @typedef {import("markdownlint-micromark").Event} Event */ -/** @typedef {import("markdownlint-micromark").ParseOptions} ParseOptions */ +/** @typedef {import("markdownlint-micromark").ParseOptions} MicromarkParseOptions */ /** @typedef {import("../lib/markdownlint.js").MicromarkToken} Token */ +/** + * Parse options. + * + * @typedef {Object} ParseOptions + * @property {boolean} [freezeTokens] Whether to freeze output Tokens. + * @property {boolean} [shimReferences] Whether to shim missing references. + */ + /** * Parses a Markdown document and returns Micromark events. * * @param {string} markdown Markdown document. - * @param {ParseOptions} [micromarkOptions] Options for micromark. - * @param {boolean} [referencesDefined] Treat references as defined. + * @param {ParseOptions} [parseOptions] Options. + * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark. * @returns {Event[]} Micromark events. */ function getEvents( markdown, - micromarkOptions = {}, - referencesDefined = true + parseOptions = {}, + micromarkParseOptions = {} ) { + // Get options + const shimReferences = Boolean(parseOptions.shimReferences); // Customize options object to add useful extensions - micromarkOptions.extensions = micromarkOptions.extensions || []; - micromarkOptions.extensions.push( + micromarkParseOptions.extensions = micromarkParseOptions.extensions || []; + micromarkParseOptions.extensions.push( micromark.directive(), micromark.gfmAutolinkLiteral(), micromark.gfmFootnote(), @@ -1016,8 +1026,8 @@ function getEvents( // Use micromark to parse document into Events const encoding = undefined; const eol = true; - const parseContext = micromark.parse(micromarkOptions); - if (referencesDefined) { + const parseContext = micromark.parse(micromarkParseOptions); + if (shimReferences) { // Customize ParseContext to treat all references as defined parseContext.defined.includes = (searchElement) => searchElement.length > 0; } @@ -1027,26 +1037,27 @@ function getEvents( } /** - * Parses a Markdown document and returns (frozen) tokens. + * Parses a Markdown document and returns micromark tokens (internal). * * @param {string} markdown Markdown document. - * @param {ParseOptions} micromarkOptions Options for micromark. - * @param {boolean} referencesDefined Treat references as defined. - * @param {number} lineDelta Offset to apply to start/end line. + * @param {ParseOptions} [parseOptions] Options. + * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark. + * @param {number} [lineDelta] Offset for start/end line. * @param {Token} [ancestor] Parent of top-most tokens. - * @returns {Token[]} Micromark tokens (frozen). + * @returns {Token[]} Micromark tokens. */ -function parseWithOffset( +function parseInternal( markdown, - micromarkOptions, - referencesDefined, - lineDelta, - ancestor + parseOptions = {}, + micromarkParseOptions = {}, + lineDelta = 0, + ancestor = undefined ) { + // Get options + const freezeTokens = Boolean(parseOptions.freezeTokens); + // Use micromark to parse document into Events - const events = getEvents( - markdown, micromarkOptions, referencesDefined - ); + const events = getEvents(markdown, parseOptions, micromarkParseOptions); // Create Token objects const document = []; @@ -1065,7 +1076,7 @@ function parseWithOffset( const history = [ root ]; let current = root; // eslint-disable-next-line jsdoc/valid-types - /** @type ParseOptions | null */ + /** @type MicromarkParseOptions | null */ let reparseOptions = null; let lines = null; let skipHtmlFlowChildren = false; @@ -1097,7 +1108,7 @@ function parseWithOffset( skipHtmlFlowChildren = true; if (!reparseOptions || !lines) { reparseOptions = { - ...micromarkOptions, + ...micromarkParseOptions, "extensions": [ { "disable": { @@ -1111,10 +1122,10 @@ function parseWithOffset( const reparseMarkdown = lines .slice(current.startLine - 1, current.endLine) .join("\n"); - const tokens = parseWithOffset( + const tokens = parseInternal( reparseMarkdown, + parseOptions, reparseOptions, - referencesDefined, current.startLine - 1, current ); @@ -1128,8 +1139,10 @@ function parseWithOffset( skipHtmlFlowChildren = false; } if (!skipHtmlFlowChildren) { - Object.freeze(current.children); - Object.freeze(current); + if (freezeTokens) { + Object.freeze(current.children); + Object.freeze(current); + } // @ts-ignore current = history.pop(); } @@ -1138,29 +1151,21 @@ function parseWithOffset( // Return document Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens }); - Object.freeze(document); + if (freezeTokens) { + Object.freeze(document); + } return document; } /** - * Parses a Markdown document and returns (frozen) tokens. + * Parses a Markdown document and returns micromark tokens. * * @param {string} markdown Markdown document. - * @param {ParseOptions} [micromarkOptions] Options for micromark. - * @param {boolean} [referencesDefined] Treat references as defined. - * @returns {Token[]} Micromark tokens (frozen). + * @param {ParseOptions} [parseOptions] Options. + * @returns {Token[]} Micromark tokens. */ -function parse( - markdown, - micromarkOptions = {}, - referencesDefined = true -) { - return parseWithOffset( - markdown, - micromarkOptions, - referencesDefined, - 0 - ); +function parse(markdown, parseOptions) { + return parseInternal(markdown, parseOptions); } module.exports = { @@ -1965,8 +1970,12 @@ function lintContent( const needMarkdownItTokens = enabledRuleList.some( (rule) => (rule.parser === "markdownit") || (rule.parser === undefined) ); + const customRulesPresent = (ruleList.length !== rules.length); // Parse content into parser tokens - const micromarkTokens = micromark.parse(content); + const micromarkTokens = micromark.parse( + content, + { "freezeTokens": customRulesPresent, "shimReferences": true } + ); // Hide the content of HTML comments from rules const preClearedContent = content; content = helpers.clearHtmlCommentText(content); @@ -5079,7 +5088,7 @@ module.exports = { if (autoLinks.length > 0) { // Re-parse with correct link/image reference definition handling const document = params.lines.join("\n"); - const tokens = parse(document, undefined, false); + const tokens = parse(document); for (const token of literalAutolinks(tokens)) { const range = [ token.startColumn, diff --git a/helpers/micromark-parse.cjs b/helpers/micromark-parse.cjs index 6c590445..d476cb04 100644 --- a/helpers/micromark-parse.cjs +++ b/helpers/micromark-parse.cjs @@ -7,26 +7,36 @@ const { isHtmlFlowComment } = require("./micromark-helpers.cjs"); const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.js"); /** @typedef {import("markdownlint-micromark").Event} Event */ -/** @typedef {import("markdownlint-micromark").ParseOptions} ParseOptions */ +/** @typedef {import("markdownlint-micromark").ParseOptions} MicromarkParseOptions */ /** @typedef {import("../lib/markdownlint.js").MicromarkToken} Token */ +/** + * Parse options. + * + * @typedef {Object} ParseOptions + * @property {boolean} [freezeTokens] Whether to freeze output Tokens. + * @property {boolean} [shimReferences] Whether to shim missing references. + */ + /** * Parses a Markdown document and returns Micromark events. * * @param {string} markdown Markdown document. - * @param {ParseOptions} [micromarkOptions] Options for micromark. - * @param {boolean} [referencesDefined] Treat references as defined. + * @param {ParseOptions} [parseOptions] Options. + * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark. * @returns {Event[]} Micromark events. */ function getEvents( markdown, - micromarkOptions = {}, - referencesDefined = true + parseOptions = {}, + micromarkParseOptions = {} ) { + // Get options + const shimReferences = Boolean(parseOptions.shimReferences); // Customize options object to add useful extensions - micromarkOptions.extensions = micromarkOptions.extensions || []; - micromarkOptions.extensions.push( + micromarkParseOptions.extensions = micromarkParseOptions.extensions || []; + micromarkParseOptions.extensions.push( micromark.directive(), micromark.gfmAutolinkLiteral(), micromark.gfmFootnote(), @@ -37,8 +47,8 @@ function getEvents( // Use micromark to parse document into Events const encoding = undefined; const eol = true; - const parseContext = micromark.parse(micromarkOptions); - if (referencesDefined) { + const parseContext = micromark.parse(micromarkParseOptions); + if (shimReferences) { // Customize ParseContext to treat all references as defined parseContext.defined.includes = (searchElement) => searchElement.length > 0; } @@ -48,26 +58,27 @@ function getEvents( } /** - * Parses a Markdown document and returns (frozen) tokens. + * Parses a Markdown document and returns micromark tokens (internal). * * @param {string} markdown Markdown document. - * @param {ParseOptions} micromarkOptions Options for micromark. - * @param {boolean} referencesDefined Treat references as defined. - * @param {number} lineDelta Offset to apply to start/end line. + * @param {ParseOptions} [parseOptions] Options. + * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark. + * @param {number} [lineDelta] Offset for start/end line. * @param {Token} [ancestor] Parent of top-most tokens. - * @returns {Token[]} Micromark tokens (frozen). + * @returns {Token[]} Micromark tokens. */ -function parseWithOffset( +function parseInternal( markdown, - micromarkOptions, - referencesDefined, - lineDelta, - ancestor + parseOptions = {}, + micromarkParseOptions = {}, + lineDelta = 0, + ancestor = undefined ) { + // Get options + const freezeTokens = Boolean(parseOptions.freezeTokens); + // Use micromark to parse document into Events - const events = getEvents( - markdown, micromarkOptions, referencesDefined - ); + const events = getEvents(markdown, parseOptions, micromarkParseOptions); // Create Token objects const document = []; @@ -86,7 +97,7 @@ function parseWithOffset( const history = [ root ]; let current = root; // eslint-disable-next-line jsdoc/valid-types - /** @type ParseOptions | null */ + /** @type MicromarkParseOptions | null */ let reparseOptions = null; let lines = null; let skipHtmlFlowChildren = false; @@ -118,7 +129,7 @@ function parseWithOffset( skipHtmlFlowChildren = true; if (!reparseOptions || !lines) { reparseOptions = { - ...micromarkOptions, + ...micromarkParseOptions, "extensions": [ { "disable": { @@ -132,10 +143,10 @@ function parseWithOffset( const reparseMarkdown = lines .slice(current.startLine - 1, current.endLine) .join("\n"); - const tokens = parseWithOffset( + const tokens = parseInternal( reparseMarkdown, + parseOptions, reparseOptions, - referencesDefined, current.startLine - 1, current ); @@ -149,8 +160,10 @@ function parseWithOffset( skipHtmlFlowChildren = false; } if (!skipHtmlFlowChildren) { - Object.freeze(current.children); - Object.freeze(current); + if (freezeTokens) { + Object.freeze(current.children); + Object.freeze(current); + } // @ts-ignore current = history.pop(); } @@ -159,29 +172,21 @@ function parseWithOffset( // Return document Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens }); - Object.freeze(document); + if (freezeTokens) { + Object.freeze(document); + } return document; } /** - * Parses a Markdown document and returns (frozen) tokens. + * Parses a Markdown document and returns micromark tokens. * * @param {string} markdown Markdown document. - * @param {ParseOptions} [micromarkOptions] Options for micromark. - * @param {boolean} [referencesDefined] Treat references as defined. - * @returns {Token[]} Micromark tokens (frozen). + * @param {ParseOptions} [parseOptions] Options. + * @returns {Token[]} Micromark tokens. */ -function parse( - markdown, - micromarkOptions = {}, - referencesDefined = true -) { - return parseWithOffset( - markdown, - micromarkOptions, - referencesDefined, - 0 - ); +function parse(markdown, parseOptions) { + return parseInternal(markdown, parseOptions); } module.exports = { diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 435971e0..5ea0eb77 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -495,8 +495,12 @@ function lintContent( const needMarkdownItTokens = enabledRuleList.some( (rule) => (rule.parser === "markdownit") || (rule.parser === undefined) ); + const customRulesPresent = (ruleList.length !== rules.length); // Parse content into parser tokens - const micromarkTokens = micromark.parse(content); + const micromarkTokens = micromark.parse( + content, + { "freezeTokens": customRulesPresent, "shimReferences": true } + ); // Hide the content of HTML comments from rules const preClearedContent = content; content = helpers.clearHtmlCommentText(content); diff --git a/lib/md034.js b/lib/md034.js index f3a98c39..13f3674f 100644 --- a/lib/md034.js +++ b/lib/md034.js @@ -73,7 +73,7 @@ module.exports = { if (autoLinks.length > 0) { // Re-parse with correct link/image reference definition handling const document = params.lines.join("\n"); - const tokens = parse(document, undefined, false); + const tokens = parse(document); for (const token of literalAutolinks(tokens)) { const range = [ token.startColumn,