Allow options.markdownItFactory to be implemented asynchronously so the markdown-it parser import can be deferred.

This commit is contained in:
David Anson 2024-12-27 21:22:14 -08:00
parent d4b981bcb3
commit 44c302fe0b
6 changed files with 448 additions and 249 deletions

View file

@ -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

View file

@ -98,7 +98,7 @@ options = {
"frontMatter": /---/,
"handleRuleFailures": false,
"noInlineConfig": false,
"markdownItFactory": () => new markdownIt()
"markdownItFactory": () => markdownIt()
};
assertLintResults(lintSync(options));

View file

@ -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;

View file

@ -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<MarkdownIt>;
/**
* Configuration options.
*/

View file

@ -443,8 +443,7 @@ function getEnabledRulesPerLineNumber(
* Lints a string containing Markdown content.
*
* @param {Rule[]} ruleList List of rules.
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
* names.
* @param {Object.<string, string[]>} 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<void> | 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<void> | 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<MarkdownIt>} Instance of the markdown-it parser.
*/
/**

View file

@ -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} */