Introduce options.markdownItFactory (and remove options.markdownItPlugins) so the markdown-it parser can be removed as a direct dependency because it is no longer used by default.

This commit is contained in:
David Anson 2024-12-25 20:42:32 -08:00
parent 3cbe1cb6c5
commit d4b981bcb3
11 changed files with 172 additions and 67 deletions

View file

@ -14,8 +14,8 @@ Match the coding style of the files you edit. Although everyone has their own
preferences and opinions, a pull request is not the right forum to debate them.
Do not add new [`dependencies` to `package.json`][dependencies]. The Markdown
parsers [`markdown-it`][markdown-it] and [`micromark`][micromark] are the
project's only dependencies.
parser [`micromark`][micromark] (and its extensions) is this project's only
dependency.
Package versions for `dependencies` and `devDependencies` should be specified
exactly (also known as "pinning"). The short explanation is that doing otherwise

View file

@ -24,7 +24,7 @@ for Markdown files. It was inspired by - and heavily influenced by - Mark
Harrison's [markdownlint][markdownlint-ruby] for Ruby. The initial rules, rule
documentation, and test cases came from that project.
`markdownlint` uses the [`micromark`][micromark] parser and honors the
`markdownlint` uses the [`micromark` parser][micromark] and honors the
[CommonMark][commonmark] specification for Markdown. It additionally supports
popular [GitHub Flavored Markdown (GFM)][gfm] syntax like autolinks and tables
as well as directives, footnotes, and math syntax - all implemented by
@ -567,28 +567,35 @@ This setting can be useful in the presence of (custom) rules that encounter
unexpected syntax and fail. By enabling this option, the linting process
is allowed to continue and report any violations that were found.
##### options.markdownItPlugins
##### options.markdownItFactory
Type: `Array` of `Array` of `Function` and plugin parameters
Type: `Function` returning an instance of a [`markdown-it` parser][markdown-it]
Specifies additional [`markdown-it` plugins][markdown-it-plugin] to use when
parsing input. Plugins can be used to support additional syntax and features for
advanced scenarios. *Deprecated.*
Provides a factory function for creating instances of the `markdown-it` parser.
[markdown-it-plugin]: https://www.npmjs.com/search?q=keywords:markdown-it-plugin
Previous versions of the `markdownlint` library declared `markdown-it` as a
direct dependency. This function makes it possible to avoid that dependency
entirely. In cases where `markdown-it` is needed, the caller is responsible for
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.
Each item in the top-level `Array` should be of the form:
For compatibility with previous versions of `markdownlint`, this function can be
implemented like:
```javascript
[ require("markdown-it-plugin"), plugin_param_0, plugin_param_1, ... ]
import markdownIt from "markdown-it";
const markdownItFactory = () => markdownIt({ "html": true });
```
> Note that `markdown-it` plugins are only called when the `markdown-it` parser
> is invoked. None of the built-in rules use the `markdown-it` parser, so
> `markdown-it` plugins will only be invoked when one or more
> [custom rules][custom-rules] that use the `markdown-it` parser are present.
> 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
> `markdown-it` parser.
[custom-rules]: #custom-rules
[markdown-it]: https://github.com/markdown-it/markdown-it
[markdown-it-plugin]: https://www.npmjs.com/search?q=keywords:markdown-it-plugin
##### options.noInlineConfig

View file

@ -7,7 +7,7 @@ import { lint as lintPromise, readConfig as readConfigPromise } from "../../lib/
import { lint as lintSync, readConfig as readConfigSync } from "../../lib/exports-sync.mjs";
import assert from "assert";
import markdownItSub from "markdown-it-sub";
import markdownIt from "markdown-it";
const markdownlintJsonPath = "../../.markdownlint.json";
const version: string = getVersion();
@ -98,7 +98,7 @@ options = {
"frontMatter": /---/,
"handleRuleFailures": false,
"noInlineConfig": false,
"markdownItPlugins": [ [ markdownItSub ] ]
"markdownItFactory": () => new markdownIt()
};
assertLintResults(lintSync(options));

View file

@ -7,6 +7,7 @@ export type LintCallback = import("./markdownlint.mjs").LintCallback;
export type LintContentCallback = import("./markdownlint.mjs").LintContentCallback;
export type LintError = import("./markdownlint.mjs").LintError;
export type LintResults = import("./markdownlint.mjs").LintResults;
export type MarkdownItFactory = import("./markdownlint.mjs").MarkdownItFactory;
export type MarkdownItToken = import("./markdownlint.mjs").MarkdownItToken;
export type MarkdownParsers = import("./markdownlint.mjs").MarkdownParsers;
export type MicromarkToken = import("./markdownlint.mjs").MicromarkToken;

View file

@ -11,6 +11,7 @@ export { resolveModule } from "./resolve-module.cjs";
/** @typedef {import("./markdownlint.mjs").LintContentCallback} LintContentCallback */
/** @typedef {import("./markdownlint.mjs").LintError} LintError */
/** @typedef {import("./markdownlint.mjs").LintResults} LintResults */
/** @typedef {import("./markdownlint.mjs").MarkdownItFactory} MarkdownItFactory */
/** @typedef {import("./markdownlint.mjs").MarkdownItToken} MarkdownItToken */
/** @typedef {import("./markdownlint.mjs").MarkdownParsers} MarkdownParsers */
/** @typedef {import("./markdownlint.mjs").MicromarkToken} MicromarkToken */

View file

@ -4,6 +4,8 @@
const { newLineRe } = require("../helpers");
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529
/** @typedef {import("markdownlint").MarkdownItFactory} MarkdownItFactory */
// @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
@ -97,7 +99,7 @@ function freezeToken(token) {
/**
* Annotate tokens with line/lineNumber and freeze them.
*
* @param {Object[]} tokens Array of markdown-it tokens.
* @param {import("markdown-it").Token[]} tokens Array of markdown-it tokens.
* @param {string[]} lines Lines of Markdown content.
* @returns {void}
*/
@ -152,21 +154,15 @@ function annotateAndFreezeTokens(tokens, lines) {
/**
* Gets an array of markdown-it tokens for the input.
*
* @param {Plugin[]} markdownItPlugins Additional plugins.
* @param {MarkdownItFactory} markdownItFactory Function to create a markdown-it parser.
* @param {string} content Markdown content.
* @param {string[]} lines Lines of Markdown content.
* @returns {MarkdownItToken} Array of markdown-it tokens.
* @returns {MarkdownItToken[]} Array of markdown-it tokens.
*/
function getMarkdownItTokens(markdownItPlugins, content, lines) {
const markdownit = require("markdown-it");
const md = markdownit({ "html": true });
for (const plugin of markdownItPlugins) {
// @ts-ignore
md.use(...plugin);
}
const tokens = md.parse(content, {});
function getMarkdownItTokens(markdownItFactory, content, lines) {
const markdownIt = markdownItFactory();
const tokens = markdownIt.parse(content, {});
annotateAndFreezeTokens(tokens, lines);
// @ts-ignore
return tokens;
};

View file

@ -357,6 +357,23 @@ export type Rule = {
*/
function: RuleFunction;
};
/**
* Method used by the markdown-it parser to parse input.
*/
export type MarkdownItParse = (src: string, env: any) => any[];
/**
* Instance of the markdown-it parser.
*/
export type MarkdownIt = {
/**
* Method to parse input.
*/
parse: MarkdownItParse;
};
/**
* Gets an instance of the markdown-it parser. Any plugins should already have been loaded.
*/
export type MarkdownItFactory = () => MarkdownIt;
/**
* Configuration options.
*/
@ -390,9 +407,9 @@ export type Options = {
*/
handleRuleFailures?: boolean;
/**
* Additional plugins.
* Function to create a markdown-it parser.
*/
markdownItPlugins?: Plugin[];
markdownItFactory?: MarkdownItFactory;
/**
* True to ignore HTML directives.
*/

View file

@ -447,7 +447,7 @@ function getEnabledRulesPerLineNumber(
* names.
* @param {string} name Identifier for the content.
* @param {string} content Markdown content.
* @param {Plugin[]} markdownItPlugins Additional plugins.
* @param {MarkdownItFactory} markdownItFactory Function to create a markdown-it parser.
* @param {Configuration} config Configuration object.
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
* @param {RegExp | null} frontMatter Regular expression for front matter.
@ -462,7 +462,7 @@ function lintContent(
aliasToRuleNames,
name,
content,
markdownItPlugins,
markdownItFactory,
config,
configParsers,
frontMatter,
@ -502,7 +502,7 @@ function lintContent(
// Parse content into lines and get markdown-it tokens
const lines = content.split(helpers.newLineRe);
const markdownitTokens = needMarkdownItTokens ?
requireMarkdownItCjs().getMarkdownItTokens(markdownItPlugins, preClearedContent, lines) :
requireMarkdownItCjs().getMarkdownItTokens(markdownItFactory, preClearedContent, lines) :
[];
// Create (frozen) parameters for rules
/** @type {MarkdownParsers} */
@ -754,10 +754,9 @@ function lintContent(
* Lints a file 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} file Path of file to lint.
* @param {Plugin[]} markdownItPlugins Additional plugins.
* @param {MarkdownItFactory} markdownItFactory Function to create a markdown-it parser.
* @param {Configuration} config Configuration object.
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
* @param {RegExp | null} frontMatter Regular expression for front matter.
@ -773,7 +772,7 @@ function lintFile(
ruleList,
aliasToRuleNames,
file,
markdownItPlugins,
markdownItFactory,
config,
configParsers,
frontMatter,
@ -793,7 +792,7 @@ function lintFile(
aliasToRuleNames,
file,
content,
markdownItPlugins,
markdownItFactory,
config,
configParsers,
frontMatter,
@ -860,7 +859,9 @@ function lintInput(options, synchronous, callback) {
const resultVersion = (options.resultVersion === undefined) ?
3 :
options.resultVersion;
const markdownItPlugins = options.markdownItPlugins || [];
const markdownItFactory =
options.markdownItFactory ||
(() => { throw new Error("The option 'markdownItFactory' was required (due to the option 'customRules' including a rule requiring the 'markdown-it' parser), but 'markdownItFactory' was not set."); });
const fs = options.fs || nodeFs;
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
const results = newResults(ruleList);
@ -892,7 +893,7 @@ function lintInput(options, synchronous, callback) {
ruleList,
aliasToRuleNames,
currentItem,
markdownItPlugins,
markdownItFactory,
config,
configParsers,
frontMatter,
@ -911,7 +912,7 @@ function lintInput(options, synchronous, callback) {
aliasToRuleNames,
currentItem,
strings[currentItem] || "",
markdownItPlugins,
markdownItFactory,
config,
configParsers,
frontMatter,
@ -1473,6 +1474,29 @@ export function getVersion() {
* @property {RuleFunction} function Rule implementation.
*/
/**
* Method used by the markdown-it parser to parse input.
*
* @callback MarkdownItParse
* @param {string} src Source string.
* @param {Object} env Environment sandbox.
* @returns {import("markdown-it").Token[]} Tokens.
*/
/**
* Instance of the markdown-it parser.
*
* @typedef MarkdownIt
* @property {MarkdownItParse} parse Method to parse input.
*/
/**
* 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.
*/
/**
* Configuration options.
*
@ -1484,7 +1508,7 @@ export function getVersion() {
* @property {RegExp | null} [frontMatter] Front matter pattern.
* @property {Object} [fs] File system implementation.
* @property {boolean} [handleRuleFailures] True to catch exceptions.
* @property {Plugin[]} [markdownItPlugins] Additional plugins.
* @property {MarkdownItFactory} [markdownItFactory] Function to create a markdown-it parser.
* @property {boolean} [noInlineConfig] True to ignore HTML directives.
* @property {number} [resultVersion] Results object version.
* @property {Object.<string, string>} [strings] Strings to lint.
@ -1501,7 +1525,7 @@ export function getVersion() {
*
* @callback ToStringCallback
* @param {boolean} [ruleAliases] True to use rule aliases.
* @returns {string}
* @returns {string} Pretty-printed results.
*/
/**

View file

@ -73,7 +73,6 @@
"node": ">=18"
},
"dependencies": {
"markdown-it": "14.1.0",
"micromark": "4.0.1",
"micromark-core-commonmark": "2.0.2",
"micromark-extension-directive": "3.0.2",
@ -100,6 +99,7 @@
"js-yaml": "4.1.0",
"json-schema-to-typescript": "15.0.4",
"jsonc-parser": "3.3.1",
"markdown-it": "14.1.0",
"markdown-it-for-inline": "2.0.1",
"markdown-it-sub": "2.0.0",
"markdown-it-sup": "2.0.0",

View file

@ -4,6 +4,7 @@ import fs from "node:fs/promises";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
import test from "ava";
import markdownIt from "markdown-it";
import { lint as lintAsync } from "markdownlint/async";
import { lint as lintPromise } from "markdownlint/promise";
import { lint as lintSync } from "markdownlint/sync";
@ -13,6 +14,8 @@ import { __filename, importWithTypeJson } from "./esm-helpers.mjs";
const packageJson = await importWithTypeJson(import.meta, "../package.json");
const { homepage, version } = packageJson;
const markdownItFactory = () => markdownIt({ "html": true });
test("customRulesV0", (t) => new Promise((resolve) => {
t.plan(4);
const customRulesMd = "./test/custom-rules.md";
@ -20,6 +23,7 @@ test("customRulesV0", (t) => new Promise((resolve) => {
const options = {
"customRules": customRules.all,
"files": [ customRulesMd ],
markdownItFactory,
"resultVersion": 0
};
lintAsync(options, function callback(err, actualResult) {
@ -92,6 +96,7 @@ test("customRulesV1", (t) => new Promise((resolve) => {
const options = {
"customRules": customRules.all,
"files": [ customRulesMd ],
markdownItFactory,
"resultVersion": 1
};
lintAsync(options, function callback(err, actualResult) {
@ -223,6 +228,7 @@ test("customRulesV2", (t) => new Promise((resolve) => {
const options = {
"customRules": customRules.all,
"files": [ customRulesMd ],
markdownItFactory,
"resultVersion": 2
};
lintAsync(options, function callback(err, actualResult) {
@ -351,6 +357,7 @@ test("customRulesConfig", (t) => new Promise((resolve) => {
},
"letters-e-x": false
},
markdownItFactory,
"resultVersion": 0
};
lintAsync(options, function callback(err, actualResult) {
@ -376,6 +383,7 @@ test("customRulesNpmPackage", (t) => new Promise((resolve) => {
require("./rules/npm"),
require("markdownlint-rule-extended-ascii")
],
markdownItFactory,
"strings": {
"string": "# Text\n\n---\n\nText ✅\n"
},
@ -555,11 +563,12 @@ test("customRulesParserUndefined", (t) => {
}
}
],
markdownItFactory,
"strings": {
"string": "# Heading\n"
}
};
return lintPromise(options).then(() => null);
return lintPromise(options);
});
test("customRulesParserNone", (t) => {
@ -583,7 +592,7 @@ test("customRulesParserNone", (t) => {
"string": "# Heading\n"
}
};
return lintPromise(options).then(() => null);
return lintPromise(options);
});
test("customRulesParserMarkdownIt", (t) => {
@ -606,11 +615,12 @@ test("customRulesParserMarkdownIt", (t) => {
}
}
],
markdownItFactory,
"strings": {
"string": "# Heading\n"
}
};
return lintPromise(options).then(() => null);
return lintPromise(options);
});
test("customRulesParserMicromark", (t) => {
@ -637,7 +647,33 @@ test("customRulesParserMicromark", (t) => {
"string": "# Heading\n"
}
};
return lintPromise(options).then(() => null);
return lintPromise(options);
});
test("customRulesMarkdownItFactoryUndefined", (t) => {
t.plan(1);
/** @type {import("markdownlint").Options} */
const options = {
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"parser": "markdownit",
"function": () => {}
}
],
"strings": {
"string": "# Heading\n"
}
};
t.throws(
() => lintSync(options),
{
"message": "The option 'markdownItFactory' was required (due to the option 'customRules' including a rule requiring the 'markdown-it' parser), but 'markdownItFactory' was not set."
},
"No exception when markdownItFactory is undefined."
);
});
test("customRulesMarkdownItParamsTokensSameObject", (t) => {
@ -657,11 +693,12 @@ test("customRulesMarkdownItParamsTokensSameObject", (t) => {
}
}
],
markdownItFactory,
"strings": {
"string": "# Heading\n"
}
};
return lintPromise(options).then(() => null);
return lintPromise(options);
});
test("customRulesMarkdownItTokensSnapshot", (t) => {
@ -680,13 +717,14 @@ test("customRulesMarkdownItTokensSnapshot", (t) => {
}
}
],
markdownItFactory,
"noInlineConfig": true
};
return fs
.readFile("./test/every-markdown-syntax.md", "utf8")
.then((content) => {
options.strings = { "content": content.split(newLineRe).join("\n") };
return lintPromise(options).then(() => null);
return lintPromise(options);
});
});
@ -712,7 +750,7 @@ test("customRulesMicromarkTokensSnapshot", (t) => {
.readFile("./test/every-markdown-syntax.md", "utf8")
.then((content) => {
options.strings = { "content": content.split(newLineRe).join("\n") };
return lintPromise(options).then(() => null);
return lintPromise(options);
});
});
@ -1665,7 +1703,8 @@ test("customRulesLintJavaScript", (t) => new Promise((resolve) => {
/** @type {import("markdownlint").Options} */
const options = {
"customRules": customRules.lintJavaScript,
"files": "test/lint-javascript.md"
"files": "test/lint-javascript.md",
markdownItFactory
};
lintAsync(options, (err, actual) => {
t.falsy(err);
@ -1693,7 +1732,8 @@ test("customRulesValidateJson", (t) => new Promise((resolve) => {
/** @type {import("markdownlint").Options} */
const options = {
"customRules": customRules.validateJson,
"files": "test/validate-json.md"
"files": "test/validate-json.md",
markdownItFactory
};
lintAsync(options, (err, actual) => {
t.falsy(err);
@ -1792,9 +1832,10 @@ test("customRulesParamsAreFrozen", (t) => {
"function": assertParamsFrozen
}
],
"files": [ "README.md" ]
"files": [ "README.md" ],
markdownItFactory
};
return lintPromise(options).then(() => null);
return lintPromise(options);
});
test("customRulesParamsAreStable", (t) => {
@ -1862,7 +1903,7 @@ test("customRulesParamsAreStable", (t) => {
"string": "# Heading"
}
};
return lintPromise(options).then(() => null);
return lintPromise(options);
});
test("customRulesAsyncReadFiles", (t) => {

View file

@ -30,6 +30,23 @@ const ajvOptions = {
"allowUnionTypes": true
};
/**
* Gets an instance of a markdown-it factory, suitable for use with options.markdownItFactory.
*
* @param {import("../lib/markdownlint.mjs").Plugin[]} markdownItPlugins Additional markdown-it plugins.
* @returns {import("../lib/markdownlint.mjs").MarkdownItFactory} Function to create a markdown-it parser.
*/
function getMarkdownItFactory(markdownItPlugins) {
return () => {
const md = markdownIt({ "html": true });
for (const markdownItPlugin of markdownItPlugins) {
// @ts-ignore
md.use(...markdownItPlugin);
}
return md;
};
}
test("simpleAsync", (t) => new Promise((resolve) => {
t.plan(2);
const options = {
@ -622,7 +639,7 @@ test("readmeHeadings", (t) => new Promise((resolve) => {
"##### options.frontMatter",
"##### options.fs",
"##### options.handleRuleFailures",
"##### options.markdownItPlugins",
"##### options.markdownItFactory",
"##### options.noInlineConfig",
"##### options.resultVersion",
"##### options.strings",
@ -1054,9 +1071,9 @@ test("markdownItPluginsSingle", (t) => new Promise((resolve) => {
},
// Use a markdown-it custom rule so the markdown-it plugin will be run
"customRules": customRules.anyBlockquote,
"markdownItPlugins": [
"markdownItFactory": getMarkdownItFactory([
[ pluginInline, "check_text_plugin", "text", () => t.true(true) ]
]
])
}, function callback(err, actual) {
t.falsy(err);
const expected = { "string": [] };
@ -1073,12 +1090,12 @@ test("markdownItPluginsMultiple", (t) => new Promise((resolve) => {
},
// Use a markdown-it custom rule so the markdown-it plugin will be run
"customRules": customRules.anyBlockquote,
"markdownItPlugins": [
"markdownItFactory": getMarkdownItFactory([
[ pluginSub ],
[ pluginSup ],
[ pluginInline, "check_sub_plugin", "sub_open", () => t.true(true) ],
[ pluginInline, "check_sup_plugin", "sup_open", () => t.true(true) ]
]
])
}, function callback(err, actual) {
t.falsy(err);
const expected = { "string": [] };
@ -1093,9 +1110,9 @@ test("markdownItPluginsNoMarkdownIt", (t) => new Promise((resolve) => {
"strings": {
"string": "# Heading\n\nText\n"
},
"markdownItPlugins": [
"markdownItFactory": getMarkdownItFactory([
[ pluginInline, "check_text_plugin", "text", () => t.fail() ]
]
])
}, function callback(err, actual) {
t.falsy(err);
const expected = { "string": [] };
@ -1115,9 +1132,9 @@ test("markdownItPluginsUnusedUncalled", (t) => new Promise((resolve) => {
},
// Use a markdown-it custom rule so the markdown-it plugin will be run
"customRules": customRules.anyBlockquote,
"markdownItPlugins": [
"markdownItFactory": getMarkdownItFactory([
[ pluginInline, "check_text_plugin", "text", () => t.fail() ]
]
])
}, function callback(err, actual) {
t.falsy(err);
const expected = { "string": [] };
@ -1184,7 +1201,8 @@ test("token-map-spans", (t) => {
}
}
],
"files": [ "./test/token-map-spans.md" ]
"files": [ "./test/token-map-spans.md" ],
"markdownItFactory": getMarkdownItFactory([])
};
lintSync(options);
});