Refactor to freeze parser tokens only when custom rules are present (negligable performance improvement).

This commit is contained in:
David Anson 2024-10-11 22:18:51 -07:00
parent a63972a666
commit 5e1b269fa5
4 changed files with 110 additions and 92 deletions

View file

@ -986,26 +986,36 @@ const { isHtmlFlowComment } = __webpack_require__(/*! ./micromark-helpers.cjs */
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = __webpack_require__(/*! ./shared.js */ "../helpers/shared.js"); const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = __webpack_require__(/*! ./shared.js */ "../helpers/shared.js");
/** @typedef {import("markdownlint-micromark").Event} Event */ /** @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 */ /** @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. * Parses a Markdown document and returns Micromark events.
* *
* @param {string} markdown Markdown document. * @param {string} markdown Markdown document.
* @param {ParseOptions} [micromarkOptions] Options for micromark. * @param {ParseOptions} [parseOptions] Options.
* @param {boolean} [referencesDefined] Treat references as defined. * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
* @returns {Event[]} Micromark events. * @returns {Event[]} Micromark events.
*/ */
function getEvents( function getEvents(
markdown, markdown,
micromarkOptions = {}, parseOptions = {},
referencesDefined = true micromarkParseOptions = {}
) { ) {
// Get options
const shimReferences = Boolean(parseOptions.shimReferences);
// Customize options object to add useful extensions // Customize options object to add useful extensions
micromarkOptions.extensions = micromarkOptions.extensions || []; micromarkParseOptions.extensions = micromarkParseOptions.extensions || [];
micromarkOptions.extensions.push( micromarkParseOptions.extensions.push(
micromark.directive(), micromark.directive(),
micromark.gfmAutolinkLiteral(), micromark.gfmAutolinkLiteral(),
micromark.gfmFootnote(), micromark.gfmFootnote(),
@ -1016,8 +1026,8 @@ function getEvents(
// Use micromark to parse document into Events // Use micromark to parse document into Events
const encoding = undefined; const encoding = undefined;
const eol = true; const eol = true;
const parseContext = micromark.parse(micromarkOptions); const parseContext = micromark.parse(micromarkParseOptions);
if (referencesDefined) { if (shimReferences) {
// Customize ParseContext to treat all references as defined // Customize ParseContext to treat all references as defined
parseContext.defined.includes = (searchElement) => searchElement.length > 0; 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 {string} markdown Markdown document.
* @param {ParseOptions} micromarkOptions Options for micromark. * @param {ParseOptions} [parseOptions] Options.
* @param {boolean} referencesDefined Treat references as defined. * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
* @param {number} lineDelta Offset to apply to start/end line. * @param {number} [lineDelta] Offset for start/end line.
* @param {Token} [ancestor] Parent of top-most tokens. * @param {Token} [ancestor] Parent of top-most tokens.
* @returns {Token[]} Micromark tokens (frozen). * @returns {Token[]} Micromark tokens.
*/ */
function parseWithOffset( function parseInternal(
markdown, markdown,
micromarkOptions, parseOptions = {},
referencesDefined, micromarkParseOptions = {},
lineDelta, lineDelta = 0,
ancestor ancestor = undefined
) { ) {
// Get options
const freezeTokens = Boolean(parseOptions.freezeTokens);
// Use micromark to parse document into Events // Use micromark to parse document into Events
const events = getEvents( const events = getEvents(markdown, parseOptions, micromarkParseOptions);
markdown, micromarkOptions, referencesDefined
);
// Create Token objects // Create Token objects
const document = []; const document = [];
@ -1065,7 +1076,7 @@ function parseWithOffset(
const history = [ root ]; const history = [ root ];
let current = root; let current = root;
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type ParseOptions | null */ /** @type MicromarkParseOptions | null */
let reparseOptions = null; let reparseOptions = null;
let lines = null; let lines = null;
let skipHtmlFlowChildren = false; let skipHtmlFlowChildren = false;
@ -1097,7 +1108,7 @@ function parseWithOffset(
skipHtmlFlowChildren = true; skipHtmlFlowChildren = true;
if (!reparseOptions || !lines) { if (!reparseOptions || !lines) {
reparseOptions = { reparseOptions = {
...micromarkOptions, ...micromarkParseOptions,
"extensions": [ "extensions": [
{ {
"disable": { "disable": {
@ -1111,10 +1122,10 @@ function parseWithOffset(
const reparseMarkdown = lines const reparseMarkdown = lines
.slice(current.startLine - 1, current.endLine) .slice(current.startLine - 1, current.endLine)
.join("\n"); .join("\n");
const tokens = parseWithOffset( const tokens = parseInternal(
reparseMarkdown, reparseMarkdown,
parseOptions,
reparseOptions, reparseOptions,
referencesDefined,
current.startLine - 1, current.startLine - 1,
current current
); );
@ -1128,8 +1139,10 @@ function parseWithOffset(
skipHtmlFlowChildren = false; skipHtmlFlowChildren = false;
} }
if (!skipHtmlFlowChildren) { if (!skipHtmlFlowChildren) {
Object.freeze(current.children); if (freezeTokens) {
Object.freeze(current); Object.freeze(current.children);
Object.freeze(current);
}
// @ts-ignore // @ts-ignore
current = history.pop(); current = history.pop();
} }
@ -1138,29 +1151,21 @@ function parseWithOffset(
// Return document // Return document
Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens }); Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens });
Object.freeze(document); if (freezeTokens) {
Object.freeze(document);
}
return 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 {string} markdown Markdown document.
* @param {ParseOptions} [micromarkOptions] Options for micromark. * @param {ParseOptions} [parseOptions] Options.
* @param {boolean} [referencesDefined] Treat references as defined. * @returns {Token[]} Micromark tokens.
* @returns {Token[]} Micromark tokens (frozen).
*/ */
function parse( function parse(markdown, parseOptions) {
markdown, return parseInternal(markdown, parseOptions);
micromarkOptions = {},
referencesDefined = true
) {
return parseWithOffset(
markdown,
micromarkOptions,
referencesDefined,
0
);
} }
module.exports = { module.exports = {
@ -1965,8 +1970,12 @@ function lintContent(
const needMarkdownItTokens = enabledRuleList.some( const needMarkdownItTokens = enabledRuleList.some(
(rule) => (rule.parser === "markdownit") || (rule.parser === undefined) (rule) => (rule.parser === "markdownit") || (rule.parser === undefined)
); );
const customRulesPresent = (ruleList.length !== rules.length);
// Parse content into parser tokens // 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 // Hide the content of HTML comments from rules
const preClearedContent = content; const preClearedContent = content;
content = helpers.clearHtmlCommentText(content); content = helpers.clearHtmlCommentText(content);
@ -5079,7 +5088,7 @@ module.exports = {
if (autoLinks.length > 0) { if (autoLinks.length > 0) {
// Re-parse with correct link/image reference definition handling // Re-parse with correct link/image reference definition handling
const document = params.lines.join("\n"); const document = params.lines.join("\n");
const tokens = parse(document, undefined, false); const tokens = parse(document);
for (const token of literalAutolinks(tokens)) { for (const token of literalAutolinks(tokens)) {
const range = [ const range = [
token.startColumn, token.startColumn,

View file

@ -7,26 +7,36 @@ const { isHtmlFlowComment } = require("./micromark-helpers.cjs");
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.js"); const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.js");
/** @typedef {import("markdownlint-micromark").Event} Event */ /** @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 */ /** @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. * Parses a Markdown document and returns Micromark events.
* *
* @param {string} markdown Markdown document. * @param {string} markdown Markdown document.
* @param {ParseOptions} [micromarkOptions] Options for micromark. * @param {ParseOptions} [parseOptions] Options.
* @param {boolean} [referencesDefined] Treat references as defined. * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
* @returns {Event[]} Micromark events. * @returns {Event[]} Micromark events.
*/ */
function getEvents( function getEvents(
markdown, markdown,
micromarkOptions = {}, parseOptions = {},
referencesDefined = true micromarkParseOptions = {}
) { ) {
// Get options
const shimReferences = Boolean(parseOptions.shimReferences);
// Customize options object to add useful extensions // Customize options object to add useful extensions
micromarkOptions.extensions = micromarkOptions.extensions || []; micromarkParseOptions.extensions = micromarkParseOptions.extensions || [];
micromarkOptions.extensions.push( micromarkParseOptions.extensions.push(
micromark.directive(), micromark.directive(),
micromark.gfmAutolinkLiteral(), micromark.gfmAutolinkLiteral(),
micromark.gfmFootnote(), micromark.gfmFootnote(),
@ -37,8 +47,8 @@ function getEvents(
// Use micromark to parse document into Events // Use micromark to parse document into Events
const encoding = undefined; const encoding = undefined;
const eol = true; const eol = true;
const parseContext = micromark.parse(micromarkOptions); const parseContext = micromark.parse(micromarkParseOptions);
if (referencesDefined) { if (shimReferences) {
// Customize ParseContext to treat all references as defined // Customize ParseContext to treat all references as defined
parseContext.defined.includes = (searchElement) => searchElement.length > 0; 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 {string} markdown Markdown document.
* @param {ParseOptions} micromarkOptions Options for micromark. * @param {ParseOptions} [parseOptions] Options.
* @param {boolean} referencesDefined Treat references as defined. * @param {MicromarkParseOptions} [micromarkParseOptions] Options for micromark.
* @param {number} lineDelta Offset to apply to start/end line. * @param {number} [lineDelta] Offset for start/end line.
* @param {Token} [ancestor] Parent of top-most tokens. * @param {Token} [ancestor] Parent of top-most tokens.
* @returns {Token[]} Micromark tokens (frozen). * @returns {Token[]} Micromark tokens.
*/ */
function parseWithOffset( function parseInternal(
markdown, markdown,
micromarkOptions, parseOptions = {},
referencesDefined, micromarkParseOptions = {},
lineDelta, lineDelta = 0,
ancestor ancestor = undefined
) { ) {
// Get options
const freezeTokens = Boolean(parseOptions.freezeTokens);
// Use micromark to parse document into Events // Use micromark to parse document into Events
const events = getEvents( const events = getEvents(markdown, parseOptions, micromarkParseOptions);
markdown, micromarkOptions, referencesDefined
);
// Create Token objects // Create Token objects
const document = []; const document = [];
@ -86,7 +97,7 @@ function parseWithOffset(
const history = [ root ]; const history = [ root ];
let current = root; let current = root;
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type ParseOptions | null */ /** @type MicromarkParseOptions | null */
let reparseOptions = null; let reparseOptions = null;
let lines = null; let lines = null;
let skipHtmlFlowChildren = false; let skipHtmlFlowChildren = false;
@ -118,7 +129,7 @@ function parseWithOffset(
skipHtmlFlowChildren = true; skipHtmlFlowChildren = true;
if (!reparseOptions || !lines) { if (!reparseOptions || !lines) {
reparseOptions = { reparseOptions = {
...micromarkOptions, ...micromarkParseOptions,
"extensions": [ "extensions": [
{ {
"disable": { "disable": {
@ -132,10 +143,10 @@ function parseWithOffset(
const reparseMarkdown = lines const reparseMarkdown = lines
.slice(current.startLine - 1, current.endLine) .slice(current.startLine - 1, current.endLine)
.join("\n"); .join("\n");
const tokens = parseWithOffset( const tokens = parseInternal(
reparseMarkdown, reparseMarkdown,
parseOptions,
reparseOptions, reparseOptions,
referencesDefined,
current.startLine - 1, current.startLine - 1,
current current
); );
@ -149,8 +160,10 @@ function parseWithOffset(
skipHtmlFlowChildren = false; skipHtmlFlowChildren = false;
} }
if (!skipHtmlFlowChildren) { if (!skipHtmlFlowChildren) {
Object.freeze(current.children); if (freezeTokens) {
Object.freeze(current); Object.freeze(current.children);
Object.freeze(current);
}
// @ts-ignore // @ts-ignore
current = history.pop(); current = history.pop();
} }
@ -159,29 +172,21 @@ function parseWithOffset(
// Return document // Return document
Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens }); Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens });
Object.freeze(document); if (freezeTokens) {
Object.freeze(document);
}
return 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 {string} markdown Markdown document.
* @param {ParseOptions} [micromarkOptions] Options for micromark. * @param {ParseOptions} [parseOptions] Options.
* @param {boolean} [referencesDefined] Treat references as defined. * @returns {Token[]} Micromark tokens.
* @returns {Token[]} Micromark tokens (frozen).
*/ */
function parse( function parse(markdown, parseOptions) {
markdown, return parseInternal(markdown, parseOptions);
micromarkOptions = {},
referencesDefined = true
) {
return parseWithOffset(
markdown,
micromarkOptions,
referencesDefined,
0
);
} }
module.exports = { module.exports = {

View file

@ -495,8 +495,12 @@ function lintContent(
const needMarkdownItTokens = enabledRuleList.some( const needMarkdownItTokens = enabledRuleList.some(
(rule) => (rule.parser === "markdownit") || (rule.parser === undefined) (rule) => (rule.parser === "markdownit") || (rule.parser === undefined)
); );
const customRulesPresent = (ruleList.length !== rules.length);
// Parse content into parser tokens // 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 // Hide the content of HTML comments from rules
const preClearedContent = content; const preClearedContent = content;
content = helpers.clearHtmlCommentText(content); content = helpers.clearHtmlCommentText(content);

View file

@ -73,7 +73,7 @@ module.exports = {
if (autoLinks.length > 0) { if (autoLinks.length > 0) {
// Re-parse with correct link/image reference definition handling // Re-parse with correct link/image reference definition handling
const document = params.lines.join("\n"); const document = params.lines.join("\n");
const tokens = parse(document, undefined, false); const tokens = parse(document);
for (const token of literalAutolinks(tokens)) { for (const token of literalAutolinks(tokens)) {
const range = [ const range = [
token.startColumn, token.startColumn,