From 44c302fe0b076058254478b144440f8f6679afd1 Mon Sep 17 00:00:00 2001 From: David Anson Date: Fri, 27 Dec 2024 21:22:14 -0800 Subject: [PATCH] Allow options.markdownItFactory to be implemented asynchronously so the markdown-it parser import can be deferred. --- README.md | 12 +- example/typescript/type-check.ts | 2 +- lib/markdownit.cjs | 7 +- lib/markdownlint.d.mts | 2 +- lib/markdownlint.mjs | 502 ++++++++++++------------ test/markdownlint-test-custom-rules.mjs | 172 ++++++++ 6 files changed, 448 insertions(+), 249 deletions(-) diff --git a/README.md b/README.md index 32e4d68a..265dd96e 100644 --- a/README.md +++ b/README.md @@ -580,14 +580,22 @@ declaring the dependency and returning an instance from this factory. If any [`markdown-it` plugins][markdown-it-plugin] are needed, they should be `use`d by the caller before returning the `markdown-it` instance. -For compatibility with previous versions of `markdownlint`, this function can be -implemented like: +For compatibility with previous versions of `markdownlint`, this function should +be similar to: ```javascript import markdownIt from "markdown-it"; const markdownItFactory = () => markdownIt({ "html": true }); ``` +When an asynchronous implementation of `lint` is being invoked (e.g., via +`markdownlint/async` or `markdownlint/promise`), this function can return a +`Promise` in order to defer the import of `markdown-it`: + +```javascript +const markdownItFactory = () => import("markdown-it").then((module) => module.default({ "html": true })); +``` + > Note that this function is only invoked when a `markdown-it` parser is > needed. None of the built-in rules use the `markdown-it` parser, so it is only > invoked when one or more [custom rules][custom-rules] are present that use the diff --git a/example/typescript/type-check.ts b/example/typescript/type-check.ts index 87171601..5abc47f0 100644 --- a/example/typescript/type-check.ts +++ b/example/typescript/type-check.ts @@ -98,7 +98,7 @@ options = { "frontMatter": /---/, "handleRuleFailures": false, "noInlineConfig": false, - "markdownItFactory": () => new markdownIt() + "markdownItFactory": () => markdownIt() }; assertLintResults(lintSync(options)); diff --git a/lib/markdownit.cjs b/lib/markdownit.cjs index 398782fb..818c85dc 100644 --- a/lib/markdownit.cjs +++ b/lib/markdownit.cjs @@ -5,7 +5,7 @@ const { newLineRe } = require("../helpers"); // @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529 -/** @typedef {import("markdownlint").MarkdownItFactory} MarkdownItFactory */ +/** @typedef {import("markdownlint").MarkdownIt} MarkdownIt */ // @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529 /** @typedef {import("markdownlint").MarkdownItToken} MarkdownItToken */ // @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529 @@ -154,13 +154,12 @@ function annotateAndFreezeTokens(tokens, lines) { /** * Gets an array of markdown-it tokens for the input. * - * @param {MarkdownItFactory} markdownItFactory Function to create a markdown-it parser. + * @param {MarkdownIt} markdownIt Instance of the markdown-it parser. * @param {string} content Markdown content. * @param {string[]} lines Lines of Markdown content. * @returns {MarkdownItToken[]} Array of markdown-it tokens. */ -function getMarkdownItTokens(markdownItFactory, content, lines) { - const markdownIt = markdownItFactory(); +function getMarkdownItTokens(markdownIt, content, lines) { const tokens = markdownIt.parse(content, {}); annotateAndFreezeTokens(tokens, lines); return tokens; diff --git a/lib/markdownlint.d.mts b/lib/markdownlint.d.mts index 523e6d68..fa56b00c 100644 --- a/lib/markdownlint.d.mts +++ b/lib/markdownlint.d.mts @@ -373,7 +373,7 @@ export type MarkdownIt = { /** * Gets an instance of the markdown-it parser. Any plugins should already have been loaded. */ -export type MarkdownItFactory = () => MarkdownIt; +export type MarkdownItFactory = () => MarkdownIt | Promise; /** * Configuration options. */ diff --git a/lib/markdownlint.mjs b/lib/markdownlint.mjs index 622e5e94..d40277d2 100644 --- a/lib/markdownlint.mjs +++ b/lib/markdownlint.mjs @@ -443,8 +443,7 @@ function getEnabledRulesPerLineNumber( * Lints a string containing Markdown content. * * @param {Rule[]} ruleList List of rules. - * @param {Object.} aliasToRuleNames Map of alias to rule - * names. + * @param {Object.} aliasToRuleNames Map of alias to rule names. * @param {string} name Identifier for the content. * @param {string} content Markdown content. * @param {MarkdownItFactory} markdownItFactory Function to create a markdown-it parser. @@ -454,6 +453,7 @@ function getEnabledRulesPerLineNumber( * @param {boolean} handleRuleFailures Whether to handle exceptions in rules. * @param {boolean} noInlineConfig Whether to allow inline configuration. * @param {number} resultVersion Version of the LintResults object to return. + * @param {boolean} synchronous Whether to execute synchronously. * @param {LintContentCallback} callback Callback (err, result) function. * @returns {void} */ @@ -469,7 +469,10 @@ function lintContent( handleRuleFailures, noInlineConfig, resultVersion, + synchronous, callback) { + // Provide a consistent error-reporting callback + const callbackError = (error) => callback(error instanceof Error ? error : new Error(error)); // Remove UTF-8 byte order marker (if present) content = content.replace(/^\uFEFF/, ""); // Remove front matter @@ -501,252 +504,267 @@ function lintContent( content = helpers.clearHtmlCommentText(content); // Parse content into lines and get markdown-it tokens const lines = content.split(helpers.newLineRe); - const markdownitTokens = needMarkdownItTokens ? - requireMarkdownItCjs().getMarkdownItTokens(markdownItFactory, preClearedContent, lines) : - []; - // Create (frozen) parameters for rules - /** @type {MarkdownParsers} */ - // @ts-ignore - const parsersMarkdownIt = Object.freeze({ - "markdownit": Object.freeze({ - "tokens": markdownitTokens - }) - }); - /** @type {MarkdownParsers} */ - // @ts-ignore - const parsersMicromark = Object.freeze({ - "micromark": Object.freeze({ - "tokens": micromarkTokens - }) - }); - /** @type {MarkdownParsers} */ - // @ts-ignore - const parsersNone = Object.freeze({}); - const paramsBase = { - name, - version, - "lines": Object.freeze(lines), - "frontMatterLines": Object.freeze(frontMatterLines) - }; - cacheInitialize({ - ...paramsBase, - "parsers": parsersMicromark, - "config": null - }); - // Function to run for each rule - let results = []; - /** - * @param {Rule} rule Rule. - * @returns {Promise | null} Promise. - */ - const forRule = (rule) => { - // Configure rule - const ruleName = rule.names[0].toUpperCase(); - const tokens = {}; - let parsers = parsersNone; - if (rule.parser === undefined) { - tokens.tokens = markdownitTokens; - parsers = parsersMarkdownIt; - } else if (rule.parser === "markdownit") { - parsers = parsersMarkdownIt; - } else if (rule.parser === "micromark") { - parsers = parsersMicromark; - } - const params = { - ...paramsBase, - ...tokens, - parsers, - "config": effectiveConfig[ruleName] - }; - // eslint-disable-next-line jsdoc/require-jsdoc - function throwError(property) { - throw new Error( - `Value of '${property}' passed to onError by '${ruleName}' is incorrect for '${name}'.`); - } - // eslint-disable-next-line jsdoc/require-jsdoc - function onError(errorInfo) { - if (!errorInfo || - !helpers.isNumber(errorInfo.lineNumber) || - (errorInfo.lineNumber < 1) || - (errorInfo.lineNumber > lines.length)) { - throwError("lineNumber"); - } - const lineNumber = errorInfo.lineNumber + frontMatterLines.length; - if (!enabledRulesPerLineNumber[lineNumber][ruleName]) { - return; - } - if (errorInfo.detail && - !helpers.isString(errorInfo.detail)) { - throwError("detail"); - } - if (errorInfo.context && - !helpers.isString(errorInfo.context)) { - throwError("context"); - } - if (errorInfo.information && - !helpers.isUrl(errorInfo.information)) { - throwError("information"); - } - if (errorInfo.range && - (!Array.isArray(errorInfo.range) || - (errorInfo.range.length !== 2) || - !helpers.isNumber(errorInfo.range[0]) || - (errorInfo.range[0] < 1) || - !helpers.isNumber(errorInfo.range[1]) || - (errorInfo.range[1] < 1) || - ((errorInfo.range[0] + errorInfo.range[1] - 1) > - lines[errorInfo.lineNumber - 1].length))) { - throwError("range"); - } - const fixInfo = errorInfo.fixInfo; - const cleanFixInfo = {}; - if (fixInfo) { - if (!helpers.isObject(fixInfo)) { - throwError("fixInfo"); - } - if (fixInfo.lineNumber !== undefined) { - if ((!helpers.isNumber(fixInfo.lineNumber) || - (fixInfo.lineNumber < 1) || - (fixInfo.lineNumber > lines.length))) { - throwError("fixInfo.lineNumber"); - } - cleanFixInfo.lineNumber = - fixInfo.lineNumber + frontMatterLines.length; - } - const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber; - if (fixInfo.editColumn !== undefined) { - if ((!helpers.isNumber(fixInfo.editColumn) || - (fixInfo.editColumn < 1) || - (fixInfo.editColumn > - lines[effectiveLineNumber - 1].length + 1))) { - throwError("fixInfo.editColumn"); - } - cleanFixInfo.editColumn = fixInfo.editColumn; - } - if (fixInfo.deleteCount !== undefined) { - if ((!helpers.isNumber(fixInfo.deleteCount) || - (fixInfo.deleteCount < -1) || - (fixInfo.deleteCount > - lines[effectiveLineNumber - 1].length))) { - throwError("fixInfo.deleteCount"); - } - cleanFixInfo.deleteCount = fixInfo.deleteCount; - } - if (fixInfo.insertText !== undefined) { - if (!helpers.isString(fixInfo.insertText)) { - throwError("fixInfo.insertText"); - } - cleanFixInfo.insertText = fixInfo.insertText; - } - } - const information = errorInfo.information || rule.information; - results.push({ - lineNumber, - "ruleName": rule.names[0], - "ruleNames": rule.names, - "ruleDescription": rule.description, - "ruleInformation": information ? information.href : null, - "errorDetail": errorInfo.detail || null, - "errorContext": errorInfo.context || null, - "errorRange": errorInfo.range ? [ ...errorInfo.range ] : null, - "fixInfo": fixInfo ? cleanFixInfo : null - }); - } - // Call (possibly external) rule function to report errors - const catchCallsOnError = (error) => onError({ - "lineNumber": 1, - "detail": `This rule threw an exception: ${error.message || error}` + // Function to run after fetching markdown-it tokens (when needed) + const lintContentInternal = (markdownitTokens) => { + // Create (frozen) parameters for rules + /** @type {MarkdownParsers} */ + // @ts-ignore + const parsersMarkdownIt = Object.freeze({ + "markdownit": Object.freeze({ + "tokens": markdownitTokens + }) }); - const invokeRuleFunction = () => rule.function(params, onError); - if (rule.asynchronous) { - // Asynchronous rule, ensure it returns a Promise - const ruleFunctionPromise = - Promise.resolve().then(invokeRuleFunction); - return handleRuleFailures ? - ruleFunctionPromise.catch(catchCallsOnError) : - ruleFunctionPromise; - } - // Synchronous rule - try { - invokeRuleFunction(); - } catch (error) { - if (handleRuleFailures) { - catchCallsOnError(error); - } else { - throw error; + /** @type {MarkdownParsers} */ + // @ts-ignore + const parsersMicromark = Object.freeze({ + "micromark": Object.freeze({ + "tokens": micromarkTokens + }) + }); + /** @type {MarkdownParsers} */ + // @ts-ignore + const parsersNone = Object.freeze({}); + const paramsBase = { + name, + version, + "lines": Object.freeze(lines), + "frontMatterLines": Object.freeze(frontMatterLines) + }; + cacheInitialize({ + ...paramsBase, + "parsers": parsersMicromark, + "config": null + }); + // Function to run for each rule + let results = []; + /** + * @param {Rule} rule Rule. + * @returns {Promise | null} Promise. + */ + const forRule = (rule) => { + // Configure rule + const ruleName = rule.names[0].toUpperCase(); + const tokens = {}; + let parsers = parsersNone; + if (rule.parser === undefined) { + tokens.tokens = markdownitTokens; + parsers = parsersMarkdownIt; + } else if (rule.parser === "markdownit") { + parsers = parsersMarkdownIt; + } else if (rule.parser === "micromark") { + parsers = parsersMicromark; } - } - return null; - }; - // eslint-disable-next-line jsdoc/require-jsdoc - function formatResults() { - // Sort results by rule name by line number - results.sort((a, b) => ( - a.ruleName.localeCompare(b.ruleName) || - a.lineNumber - b.lineNumber - )); - if (resultVersion < 3) { - // Remove fixInfo and multiple errors for the same rule and line number - const noPrevious = { - "ruleName": null, - "lineNumber": -1 + const params = { + ...paramsBase, + ...tokens, + parsers, + "config": effectiveConfig[ruleName] }; - results = results.filter((error, index, array) => { - delete error.fixInfo; - const previous = array[index - 1] || noPrevious; - return ( - (error.ruleName !== previous.ruleName) || - (error.lineNumber !== previous.lineNumber) - ); + // eslint-disable-next-line jsdoc/require-jsdoc + function throwError(property) { + throw new Error( + `Value of '${property}' passed to onError by '${ruleName}' is incorrect for '${name}'.`); + } + // eslint-disable-next-line jsdoc/require-jsdoc + function onError(errorInfo) { + if (!errorInfo || + !helpers.isNumber(errorInfo.lineNumber) || + (errorInfo.lineNumber < 1) || + (errorInfo.lineNumber > lines.length)) { + throwError("lineNumber"); + } + const lineNumber = errorInfo.lineNumber + frontMatterLines.length; + if (!enabledRulesPerLineNumber[lineNumber][ruleName]) { + return; + } + if (errorInfo.detail && + !helpers.isString(errorInfo.detail)) { + throwError("detail"); + } + if (errorInfo.context && + !helpers.isString(errorInfo.context)) { + throwError("context"); + } + if (errorInfo.information && + !helpers.isUrl(errorInfo.information)) { + throwError("information"); + } + if (errorInfo.range && + (!Array.isArray(errorInfo.range) || + (errorInfo.range.length !== 2) || + !helpers.isNumber(errorInfo.range[0]) || + (errorInfo.range[0] < 1) || + !helpers.isNumber(errorInfo.range[1]) || + (errorInfo.range[1] < 1) || + ((errorInfo.range[0] + errorInfo.range[1] - 1) > + lines[errorInfo.lineNumber - 1].length))) { + throwError("range"); + } + const fixInfo = errorInfo.fixInfo; + const cleanFixInfo = {}; + if (fixInfo) { + if (!helpers.isObject(fixInfo)) { + throwError("fixInfo"); + } + if (fixInfo.lineNumber !== undefined) { + if ((!helpers.isNumber(fixInfo.lineNumber) || + (fixInfo.lineNumber < 1) || + (fixInfo.lineNumber > lines.length))) { + throwError("fixInfo.lineNumber"); + } + cleanFixInfo.lineNumber = + fixInfo.lineNumber + frontMatterLines.length; + } + const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber; + if (fixInfo.editColumn !== undefined) { + if ((!helpers.isNumber(fixInfo.editColumn) || + (fixInfo.editColumn < 1) || + (fixInfo.editColumn > + lines[effectiveLineNumber - 1].length + 1))) { + throwError("fixInfo.editColumn"); + } + cleanFixInfo.editColumn = fixInfo.editColumn; + } + if (fixInfo.deleteCount !== undefined) { + if ((!helpers.isNumber(fixInfo.deleteCount) || + (fixInfo.deleteCount < -1) || + (fixInfo.deleteCount > + lines[effectiveLineNumber - 1].length))) { + throwError("fixInfo.deleteCount"); + } + cleanFixInfo.deleteCount = fixInfo.deleteCount; + } + if (fixInfo.insertText !== undefined) { + if (!helpers.isString(fixInfo.insertText)) { + throwError("fixInfo.insertText"); + } + cleanFixInfo.insertText = fixInfo.insertText; + } + } + const information = errorInfo.information || rule.information; + results.push({ + lineNumber, + "ruleName": rule.names[0], + "ruleNames": rule.names, + "ruleDescription": rule.description, + "ruleInformation": information ? information.href : null, + "errorDetail": errorInfo.detail || null, + "errorContext": errorInfo.context || null, + "errorRange": errorInfo.range ? [ ...errorInfo.range ] : null, + "fixInfo": fixInfo ? cleanFixInfo : null + }); + } + // Call (possibly external) rule function to report errors + const catchCallsOnError = (error) => onError({ + "lineNumber": 1, + "detail": `This rule threw an exception: ${error.message || error}` }); - } - if (resultVersion === 0) { - // Return a dictionary of rule->[line numbers] - const dictionary = {}; - for (const error of results) { - const ruleLines = dictionary[error.ruleName] || []; - ruleLines.push(error.lineNumber); - dictionary[error.ruleName] = ruleLines; + const invokeRuleFunction = () => rule.function(params, onError); + if (rule.asynchronous) { + // Asynchronous rule, ensure it returns a Promise + const ruleFunctionPromise = + Promise.resolve().then(invokeRuleFunction); + return handleRuleFailures ? + ruleFunctionPromise.catch(catchCallsOnError) : + ruleFunctionPromise; } - // @ts-ignore - results = dictionary; - } else if (resultVersion === 1) { - // Use ruleAlias instead of ruleNames - for (const error of results) { - error.ruleAlias = error.ruleNames[1] || error.ruleName; - delete error.ruleNames; + // Synchronous rule + try { + invokeRuleFunction(); + } catch (error) { + if (handleRuleFailures) { + catchCallsOnError(error); + } else { + throw error; + } } - } else { - // resultVersion 2 or 3: Remove unwanted ruleName - for (const error of results) { - delete error.ruleName; + return null; + }; + const formatResults = () => { + // Sort results by rule name by line number + results.sort((a, b) => ( + a.ruleName.localeCompare(b.ruleName) || + a.lineNumber - b.lineNumber + )); + if (resultVersion < 3) { + // Remove fixInfo and multiple errors for the same rule and line number + const noPrevious = { + "ruleName": null, + "lineNumber": -1 + }; + results = results.filter((error, index, array) => { + delete error.fixInfo; + const previous = array[index - 1] || noPrevious; + return ( + (error.ruleName !== previous.ruleName) || + (error.lineNumber !== previous.lineNumber) + ); + }); } + if (resultVersion === 0) { + // Return a dictionary of rule->[line numbers] + const dictionary = {}; + for (const error of results) { + const ruleLines = dictionary[error.ruleName] || []; + ruleLines.push(error.lineNumber); + dictionary[error.ruleName] = ruleLines; + } + // @ts-ignore + results = dictionary; + } else if (resultVersion === 1) { + // Use ruleAlias instead of ruleNames + for (const error of results) { + error.ruleAlias = error.ruleNames[1] || error.ruleName; + delete error.ruleNames; + } + } else { + // resultVersion 2 or 3: Remove unwanted ruleName + for (const error of results) { + delete error.ruleName; + } + } + return results; + }; + // Run all rules + const ruleListAsync = enabledRuleList.filter((rule) => rule.asynchronous); + const ruleListSync = enabledRuleList.filter((rule) => !rule.asynchronous); + const ruleListAsyncFirst = [ + ...ruleListAsync, + ...ruleListSync + ]; + const callbackSuccess = () => callback(null, formatResults()); + try { + const ruleResults = ruleListAsyncFirst.map(forRule); + if (ruleListAsync.length > 0) { + Promise.all(ruleResults.slice(0, ruleListAsync.length)) + .then(callbackSuccess) + .catch(callbackError); + } else { + callbackSuccess(); + } + } catch (error) { + callbackError(error); + } finally { + cacheInitialize(); } - return results; - } - // Run all rules - const ruleListAsync = enabledRuleList.filter((rule) => rule.asynchronous); - const ruleListSync = enabledRuleList.filter((rule) => !rule.asynchronous); - const ruleListAsyncFirst = [ - ...ruleListAsync, - ...ruleListSync - ]; - const callbackSuccess = () => callback(null, formatResults()); - const callbackError = - (error) => callback(error instanceof Error ? error : new Error(error)); - try { - const ruleResults = ruleListAsyncFirst.map(forRule); - if (ruleListAsync.length > 0) { - Promise.all(ruleResults.slice(0, ruleListAsync.length)) - .then(callbackSuccess) - .catch(callbackError); - } else { - callbackSuccess(); - } - } catch (error) { - callbackError(error); - } finally { - cacheInitialize(); + }; + if (!needMarkdownItTokens || synchronous) { + // Need/able to call into markdown-it and lintContentInternal synchronously + const markdownItTokens = needMarkdownItTokens ? + requireMarkdownItCjs().getMarkdownItTokens(markdownItFactory(), preClearedContent, lines) : + []; + lintContentInternal(markdownItTokens); + } else { + // Need to call into markdown-it and lintContentInternal asynchronously + Promise.all([ + // eslint-disable-next-line no-inline-comments + import(/* webpackMode: "eager" */ "./markdownit.cjs"), + // eslint-disable-next-line no-promise-executor-return + new Promise((resolve) => resolve(markdownItFactory())) + ]).then(([ markdownitCjs, markdownIt ]) => { + const markdownItTokens = markdownitCjs.getMarkdownItTokens(markdownIt, preClearedContent, lines); + lintContentInternal(markdownItTokens); + }).catch(callbackError); } } @@ -799,6 +817,7 @@ function lintFile( handleRuleFailures, noInlineConfig, resultVersion, + synchronous, callback ); } @@ -919,6 +938,7 @@ function lintInput(options, synchronous, callback) { handleRuleFailures, noInlineConfig, resultVersion, + synchronous, lintWorkerCallback ); } else if (concurrency === 0) { @@ -1494,7 +1514,7 @@ export function getVersion() { * Gets an instance of the markdown-it parser. Any plugins should already have been loaded. * * @callback MarkdownItFactory - * @returns {MarkdownIt} Instance of the markdown-it parser. + * @returns {MarkdownIt|Promise} Instance of the markdown-it parser. */ /** diff --git a/test/markdownlint-test-custom-rules.mjs b/test/markdownlint-test-custom-rules.mjs index b9dd4d25..1a78c117 100644 --- a/test/markdownlint-test-custom-rules.mjs +++ b/test/markdownlint-test-custom-rules.mjs @@ -676,6 +676,178 @@ test("customRulesMarkdownItFactoryUndefined", (t) => { ); }); +test("customRulesMarkdownItFactoryNotNeededSync", (t) => { + t.plan(1); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "none", + "function": () => {} + } + ], + "markdownItFactory": () => t.fail(), + "strings": { + "string": "# Heading\n" + } + }; + t.pass(); + return lintSync(options); +}); + +test("customRulesMarkdownItFactoryNeededSync", (t) => { + t.plan(1); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "markdownit", + "function": () => {} + } + ], + "markdownItFactory": () => t.pass() && markdownIt(), + "strings": { + "string": "# Heading\n" + } + }; + return lintSync(options); +}); + +test("customRulesMarkdownItFactoryNotNeededAsync", (t) => { + t.plan(1); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "none", + "function": () => {} + } + ], + "markdownItFactory": () => t.fail(), + "strings": { + "string": "# Heading\n" + } + }; + t.pass(); + return lintPromise(options); +}); + +test("customRulesMarkdownItFactoryNeededAsyncRunsSync", (t) => { + t.plan(1); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "markdownit", + "function": () => {} + } + ], + "markdownItFactory": () => t.pass() && markdownIt(), + "strings": { + "string": "# Heading\n" + } + }; + return lintPromise(options); +}); + +test("customRulesMarkdownItFactoryNeededAsyncRunsAsync", (t) => { + t.plan(1); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "markdownit", + "function": () => {} + } + ], + "markdownItFactory": () => t.pass() && Promise.resolve(markdownIt()), + "strings": { + "string": "# Heading\n" + } + }; + return lintPromise(options); +}); + +test("customRulesMarkdownItFactoryNeededAsyncRunsAsyncWithImport", (t) => { + t.plan(1); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "markdownit", + "function": () => {} + } + ], + "markdownItFactory": () => import("markdown-it").then((module) => t.pass() && module.default()), + "strings": { + "string": "# Heading\n" + } + }; + return lintPromise(options); +}); + +test("customRulesMarkdownItInstanceCanBeReusedSync", (t) => { + t.plan(1); + const markdownItInstance = markdownItFactory(); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "markdownit", + "function": () => {} + } + ], + "markdownItFactory": () => markdownItInstance, + "strings": { + "string": "# Heading" + } + }; + t.deepEqual(lintSync(options), lintSync(options)); +}); + +test("customRulesMarkdownItInstanceCanBeReusedAsync", async(t) => { + t.plan(1); + const markdownItInstance = markdownItFactory(); + /** @type {import("markdownlint").Options} */ + const options = { + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "parser": "markdownit", + "function": () => {} + } + ], + "markdownItFactory": () => Promise.resolve(markdownItInstance), + "strings": { + "string": "# Heading" + } + }; + t.deepEqual(await lintPromise(options), await lintPromise(options)); +}); + test("customRulesMarkdownItParamsTokensSameObject", (t) => { t.plan(1); /** @type {import("markdownlint").Options} */