From 4e30462216bf9093b526562454b74f79ea6166d3 Mon Sep 17 00:00:00 2001 From: David Anson Date: Sun, 6 Oct 2024 17:24:44 -0700 Subject: [PATCH] Promote applyFix and applyFixes helpers into core library. --- README.md | 46 ++- demo/default.js | 3 +- demo/markdownlint-browser.js | 262 +++++++------- example/typescript/type-check.ts | 30 ++ helpers/README.md | 17 - helpers/helpers.js | 127 ------- lib/markdownlint.d.ts | 40 ++- lib/markdownlint.js | 125 +++++++ package.json | 2 +- test/markdownlint-test-fixes.js | 532 ++++++++++++++++++++++++++++ test/markdownlint-test-helpers.js | 529 +-------------------------- test/markdownlint-test-scenarios.js | 6 +- test/markdownlint-test.js | 2 +- 13 files changed, 898 insertions(+), 823 deletions(-) create mode 100644 test/markdownlint-test-fixes.js diff --git a/README.md b/README.md index 06fb8aa2..0485b1b6 100644 --- a/README.md +++ b/README.md @@ -768,6 +768,44 @@ Type: `Object` Configuration object. +### Fixing + +Rules that can be fixed automatically include a `fixInfo` property which is +outlined in the [documentation for custom rules](doc/CustomRules.md#authoring). +To apply fixes consistently, the `applyFix`/`applyFixes` methods may be used: + +```javascript +/** + * Applies the specified fix to a Markdown content line. + * + * @param {string} line Line of Markdown content. + * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {string} [lineEnding] Line ending to use. + * @returns {string | null} Fixed content or null if deleted. + */ +function applyFix(line, fixInfo, lineEnding = "\n") { ... } + +/** + * Applies as many of the specified fixes as possible to Markdown content. + * + * @param {string} input Lines of Markdown content. + * @param {RuleOnErrorInfo[]} errors RuleOnErrorInfo instances. + * @returns {string} Fixed content. + */ +function applyFixes(input, errors) { ... } +``` + +Invoking `applyFixes` with the results of a call to lint can be done like so: + +```javascript +const { "sync": markdownlintSync, applyFixes } = require("markdownlint"); + +function fixMarkdownlintViolations(content) { + const fixResults = markdownlintSync({ strings: { content } }); + return applyFixes(content, fixResults.content); +} +``` + ## Usage Invoke `markdownlint` and use the `result` object's `toString` method: @@ -934,14 +972,6 @@ bad.md: 1: MD041/first-line-heading/first-line-h1 First line in a file should be Use --force to continue. ``` -### Fixing - -Rules that can be fixed automatically include a `fixInfo` property which is -outlined in the [documentation for custom rules](doc/CustomRules.md#authoring). -To apply those fixes more easily, the `applyFixes` method in -[markdownlint-rule-helpers](helpers/README.md#applying-recommended-fixes) may -be used. - ## Browser `markdownlint` also works in the browser. diff --git a/demo/default.js b/demo/default.js index 890b1b73..73eeaa65 100644 --- a/demo/default.js +++ b/demo/default.js @@ -4,7 +4,6 @@ // Dependencies var markdownit = globalThis.markdownit; var markdownlint = globalThis.markdownlint.library; - var helpers = globalThis.markdownlint.helpers; var micromark = globalThis.micromarkBrowser; var micromarkHtml = globalThis.micromarkHtmlBrowser; @@ -184,7 +183,7 @@ var errors = e.shiftKey ? allLintErrors : [ JSON.parse(decodeURIComponent(e.target.target)) ]; - var fixed = helpers.applyFixes(markdown.value, errors); + var fixed = markdownlint.applyFixes(markdown.value, errors); markdown.value = fixed; onMarkdownInput(); e.preventDefault(); diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index ebc5e526..80dbfa01 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -22,17 +22,7 @@ module.exports.newLineRe = newLineRe; module.exports.nextLinesRe = nextLinesRe; /** @typedef {import("../lib/markdownlint.js").RuleOnError} RuleOnError */ -/** @typedef {import("../lib/markdownlint.js").RuleOnErrorInfo} RuleOnErrorInfo */ /** @typedef {import("../lib/markdownlint.js").RuleOnErrorFixInfo} RuleOnErrorFixInfo */ -/** - * RuleOnErrorInfo with common optional properties filled in. - * - * @typedef {Object} RuleOnErrorFixInfoNormalized - * @property {number} lineNumber Line number (1-based). - * @property {number} editColumn Column of the fix (1-based). - * @property {number} deleteCount Count of characters to delete. - * @property {string} insertText Text to insert (after deleting). - */ // Regular expression for matching common front matter (YAML and TOML) module.exports.frontMatterRe = @@ -566,123 +556,6 @@ function getPreferredLineEnding(input, os) { } module.exports.getPreferredLineEnding = getPreferredLineEnding; -/** - * Normalizes the fields of a RuleOnErrorFixInfo instance. - * - * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. - * @param {number} [lineNumber] Line number. - * @returns {RuleOnErrorFixInfoNormalized} Normalized RuleOnErrorFixInfo instance. - */ -function normalizeFixInfo(fixInfo, lineNumber = 0) { - return { - "lineNumber": fixInfo.lineNumber || lineNumber, - "editColumn": fixInfo.editColumn || 1, - "deleteCount": fixInfo.deleteCount || 0, - "insertText": fixInfo.insertText || "" - }; -} - -/** - * Fixes the specified error on a line of Markdown content. - * - * @param {string} line Line of Markdown content. - * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. - * @param {string} [lineEnding] Line ending to use. - * @returns {string | null} Fixed content. - */ -function applyFix(line, fixInfo, lineEnding) { - const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo); - const editIndex = editColumn - 1; - return (deleteCount === -1) ? - null : - line.slice(0, editIndex) + - insertText.replace(/\n/g, lineEnding || "\n") + - line.slice(editIndex + deleteCount); -} -module.exports.applyFix = applyFix; - -/** - * Applies as many fixes as possible to Markdown content. - * - * @param {string} input Lines of Markdown content. - * @param {RuleOnErrorInfo[]} errors RuleOnErrorInfo instances. - * @returns {string} Corrected content. - */ -function applyFixes(input, errors) { - const lineEnding = getPreferredLineEnding(input, __webpack_require__(/*! node:os */ "?0176")); - const lines = input.split(newLineRe); - // Normalize fixInfo objects - let fixInfos = errors - .filter((error) => error.fixInfo) - // @ts-ignore - .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber)); - // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short - fixInfos.sort((a, b) => { - const aDeletingLine = (a.deleteCount === -1); - const bDeletingLine = (b.deleteCount === -1); - return ( - (b.lineNumber - a.lineNumber) || - (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) || - (b.editColumn - a.editColumn) || - (b.insertText.length - a.insertText.length) - ); - }); - // Remove duplicate entries (needed for following collapse step) - // eslint-disable-next-line jsdoc/valid-types - /** @type RuleOnErrorFixInfo */ - let lastFixInfo = {}; - fixInfos = fixInfos.filter((fixInfo) => { - const unique = ( - (fixInfo.lineNumber !== lastFixInfo.lineNumber) || - (fixInfo.editColumn !== lastFixInfo.editColumn) || - (fixInfo.deleteCount !== lastFixInfo.deleteCount) || - (fixInfo.insertText !== lastFixInfo.insertText) - ); - lastFixInfo = fixInfo; - return unique; - }); - // Collapse insert/no-delete and no-insert/delete for same line/column - lastFixInfo = { - "lineNumber": -1 - }; - for (const fixInfo of fixInfos) { - if ( - (fixInfo.lineNumber === lastFixInfo.lineNumber) && - (fixInfo.editColumn === lastFixInfo.editColumn) && - !fixInfo.insertText && - (fixInfo.deleteCount > 0) && - lastFixInfo.insertText && - !lastFixInfo.deleteCount) { - fixInfo.insertText = lastFixInfo.insertText; - lastFixInfo.lineNumber = 0; - } - lastFixInfo = fixInfo; - } - fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber); - // Apply all (remaining/updated) fixes - let lastLineIndex = -1; - let lastEditIndex = -1; - for (const fixInfo of fixInfos) { - const { lineNumber, editColumn, deleteCount } = fixInfo; - const lineIndex = lineNumber - 1; - const editIndex = editColumn - 1; - if ( - (lineIndex !== lastLineIndex) || - (deleteCount === -1) || - ((editIndex + deleteCount) <= - (lastEditIndex - ((deleteCount > 0) ? 0 : 1))) - ) { - // @ts-ignore - lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding); - } - lastLineIndex = lineIndex; - lastEditIndex = editIndex; - } - // Return corrected input - return lines.filter((line) => line !== null).join(lineEnding); -} -module.exports.applyFixes = applyFixes; - /** * Expands a path with a tilde to an absolute path. * @@ -766,16 +639,6 @@ module.exports = micromarkBrowser; /***/ }), -/***/ "?0176": -/*!*************************!*\ - !*** node:os (ignored) ***! - \*************************/ -/***/ (() => { - -/* (ignored) */ - -/***/ }), - /***/ "?d0ee": /*!*************************!*\ !*** node:fs (ignored) ***! @@ -2849,6 +2712,119 @@ function readConfigSync(file, parsers, fs) { return config; } +/** + * Normalizes the fields of a RuleOnErrorFixInfo instance. + * + * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {number} [lineNumber] Line number. + * @returns {RuleOnErrorFixInfoNormalized} Normalized RuleOnErrorFixInfo instance. + */ +function normalizeFixInfo(fixInfo, lineNumber = 0) { + return { + "lineNumber": fixInfo.lineNumber || lineNumber, + "editColumn": fixInfo.editColumn || 1, + "deleteCount": fixInfo.deleteCount || 0, + "insertText": fixInfo.insertText || "" + }; +} + +/** + * Applies the specified fix to a Markdown content line. + * + * @param {string} line Line of Markdown content. + * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {string} [lineEnding] Line ending to use. + * @returns {string | null} Fixed content or null if deleted. + */ +function applyFix(line, fixInfo, lineEnding = "\n") { + const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo); + const editIndex = editColumn - 1; + return (deleteCount === -1) ? + null : + line.slice(0, editIndex) + insertText.replace(/\n/g, lineEnding) + line.slice(editIndex + deleteCount); +} + +/** + * Applies as many of the specified fixes as possible to Markdown content. + * + * @param {string} input Lines of Markdown content. + * @param {RuleOnErrorInfo[]} errors RuleOnErrorInfo instances. + * @returns {string} Fixed content. + */ +function applyFixes(input, errors) { + const lineEnding = helpers.getPreferredLineEnding(input, __webpack_require__(/*! node:os */ "?e6c4")); + const lines = input.split(helpers.newLineRe); + // Normalize fixInfo objects + let fixInfos = errors + .filter((error) => error.fixInfo) + // @ts-ignore + .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber)); + // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short + fixInfos.sort((a, b) => { + const aDeletingLine = (a.deleteCount === -1); + const bDeletingLine = (b.deleteCount === -1); + return ( + (b.lineNumber - a.lineNumber) || + (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) || + (b.editColumn - a.editColumn) || + (b.insertText.length - a.insertText.length) + ); + }); + // Remove duplicate entries (needed for following collapse step) + // eslint-disable-next-line jsdoc/valid-types + /** @type RuleOnErrorFixInfo */ + let lastFixInfo = {}; + fixInfos = fixInfos.filter((fixInfo) => { + const unique = ( + (fixInfo.lineNumber !== lastFixInfo.lineNumber) || + (fixInfo.editColumn !== lastFixInfo.editColumn) || + (fixInfo.deleteCount !== lastFixInfo.deleteCount) || + (fixInfo.insertText !== lastFixInfo.insertText) + ); + lastFixInfo = fixInfo; + return unique; + }); + // Collapse insert/no-delete and no-insert/delete for same line/column + lastFixInfo = { + "lineNumber": -1 + }; + for (const fixInfo of fixInfos) { + if ( + (fixInfo.lineNumber === lastFixInfo.lineNumber) && + (fixInfo.editColumn === lastFixInfo.editColumn) && + !fixInfo.insertText && + (fixInfo.deleteCount > 0) && + lastFixInfo.insertText && + !lastFixInfo.deleteCount) { + fixInfo.insertText = lastFixInfo.insertText; + lastFixInfo.lineNumber = 0; + } + lastFixInfo = fixInfo; + } + fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber); + // Apply all (remaining/updated) fixes + let lastLineIndex = -1; + let lastEditIndex = -1; + for (const fixInfo of fixInfos) { + const { lineNumber, editColumn, deleteCount } = fixInfo; + const lineIndex = lineNumber - 1; + const editIndex = editColumn - 1; + if ( + (lineIndex !== lastLineIndex) || + (deleteCount === -1) || + ((editIndex + deleteCount) <= + (lastEditIndex - ((deleteCount > 0) ? 0 : 1))) + ) { + // @ts-ignore + lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding); + } + lastLineIndex = lineIndex; + lastEditIndex = editIndex; + } + // Return corrected input + return lines.filter((line) => line !== null).join(lineEnding); +} + /** * Gets the (semantic) version of the library. * @@ -2868,6 +2844,8 @@ markdownlint.promises = { "extendConfig": extendConfigPromise, "readConfig": readConfigPromise }; +markdownlint.applyFix = applyFix; +markdownlint.applyFixes = applyFixes; module.exports = markdownlint; // Type declarations @@ -2986,6 +2964,16 @@ module.exports = markdownlint; * @property {string} [insertText] Text to insert (after deleting). */ +/** + * RuleOnErrorInfo with all optional properties present. + * + * @typedef {Object} RuleOnErrorFixInfoNormalized + * @property {number} lineNumber Line number (1-based). + * @property {number} editColumn Column of the fix (1-based). + * @property {number} deleteCount Count of characters to delete. + * @property {string} insertText Text to insert (after deleting). + */ + /** * Rule definition. * diff --git a/example/typescript/type-check.ts b/example/typescript/type-check.ts index b33351da..eb9c13b1 100644 --- a/example/typescript/type-check.ts +++ b/example/typescript/type-check.ts @@ -166,6 +166,36 @@ markdownlint(options, assertLintResultsCallback); assertLintResultsCallback(null, await markdownlint.promises.markdownlint(options)); })(); +assert.equal( + markdownlint.applyFix( + "# Fixing\n", + { + "insertText": "Head", + "editColumn": 3, + "deleteCount": 3 + }, + "\n" + ), + "# Heading\n" +); + +assert.equal( + markdownlint.applyFixes( + "# Fixing\n", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "Head", + "editColumn": 3, + "deleteCount": 3 + } + } + ] + ), + "# Heading\n" +); + const configuration: markdownlint.Configuration = { "custom-rule": true, "no-hard-tabs": false, diff --git a/helpers/README.md b/helpers/README.md index 6153b5bf..73901d4d 100644 --- a/helpers/README.md +++ b/helpers/README.md @@ -17,22 +17,6 @@ change from release to release. There are brief descriptive comments above each function, but no [JSDoc][jsdoc] annotations. That said, some of what's here will be useful to custom rule authors and may avoid duplicating code. -## Examples - -### Applying Recommended Fixes - -```javascript -const { "sync": markdownlintSync } = require("markdownlint"); -const markdownlintRuleHelpers = require("markdownlint-rule-helpers"); - -function fixMarkdownlintViolations(content) { - const fixResults = markdownlintSync({ strings: { content } }); - return markdownlintRuleHelpers.applyFixes(content, fixResults.content); -} -``` - -See also: [`markdownlint` built-in rule implementations][lib]. - ## Tests *None* - The entire body of code is tested to 100% coverage by the core @@ -40,7 +24,6 @@ See also: [`markdownlint` built-in rule implementations][lib]. [custom-rules]: https://github.com/DavidAnson/markdownlint/blob/v0.35.0/doc/CustomRules.md [jsdoc]: https://en.m.wikipedia.org/wiki/JSDoc -[lib]: https://github.com/DavidAnson/markdownlint/tree/v0.35.0/lib [markdown]: https://en.wikipedia.org/wiki/Markdown [markdownlint]: https://github.com/DavidAnson/markdownlint [rules]: https://github.com/DavidAnson/markdownlint/blob/v0.35.0/doc/Rules.md diff --git a/helpers/helpers.js b/helpers/helpers.js index 10a8097d..9c7ae4e3 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -10,17 +10,7 @@ module.exports.newLineRe = newLineRe; module.exports.nextLinesRe = nextLinesRe; /** @typedef {import("../lib/markdownlint.js").RuleOnError} RuleOnError */ -/** @typedef {import("../lib/markdownlint.js").RuleOnErrorInfo} RuleOnErrorInfo */ /** @typedef {import("../lib/markdownlint.js").RuleOnErrorFixInfo} RuleOnErrorFixInfo */ -/** - * RuleOnErrorInfo with common optional properties filled in. - * - * @typedef {Object} RuleOnErrorFixInfoNormalized - * @property {number} lineNumber Line number (1-based). - * @property {number} editColumn Column of the fix (1-based). - * @property {number} deleteCount Count of characters to delete. - * @property {string} insertText Text to insert (after deleting). - */ // Regular expression for matching common front matter (YAML and TOML) module.exports.frontMatterRe = @@ -554,123 +544,6 @@ function getPreferredLineEnding(input, os) { } module.exports.getPreferredLineEnding = getPreferredLineEnding; -/** - * Normalizes the fields of a RuleOnErrorFixInfo instance. - * - * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. - * @param {number} [lineNumber] Line number. - * @returns {RuleOnErrorFixInfoNormalized} Normalized RuleOnErrorFixInfo instance. - */ -function normalizeFixInfo(fixInfo, lineNumber = 0) { - return { - "lineNumber": fixInfo.lineNumber || lineNumber, - "editColumn": fixInfo.editColumn || 1, - "deleteCount": fixInfo.deleteCount || 0, - "insertText": fixInfo.insertText || "" - }; -} - -/** - * Fixes the specified error on a line of Markdown content. - * - * @param {string} line Line of Markdown content. - * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. - * @param {string} [lineEnding] Line ending to use. - * @returns {string | null} Fixed content. - */ -function applyFix(line, fixInfo, lineEnding) { - const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo); - const editIndex = editColumn - 1; - return (deleteCount === -1) ? - null : - line.slice(0, editIndex) + - insertText.replace(/\n/g, lineEnding || "\n") + - line.slice(editIndex + deleteCount); -} -module.exports.applyFix = applyFix; - -/** - * Applies as many fixes as possible to Markdown content. - * - * @param {string} input Lines of Markdown content. - * @param {RuleOnErrorInfo[]} errors RuleOnErrorInfo instances. - * @returns {string} Corrected content. - */ -function applyFixes(input, errors) { - const lineEnding = getPreferredLineEnding(input, require("node:os")); - const lines = input.split(newLineRe); - // Normalize fixInfo objects - let fixInfos = errors - .filter((error) => error.fixInfo) - // @ts-ignore - .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber)); - // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short - fixInfos.sort((a, b) => { - const aDeletingLine = (a.deleteCount === -1); - const bDeletingLine = (b.deleteCount === -1); - return ( - (b.lineNumber - a.lineNumber) || - (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) || - (b.editColumn - a.editColumn) || - (b.insertText.length - a.insertText.length) - ); - }); - // Remove duplicate entries (needed for following collapse step) - // eslint-disable-next-line jsdoc/valid-types - /** @type RuleOnErrorFixInfo */ - let lastFixInfo = {}; - fixInfos = fixInfos.filter((fixInfo) => { - const unique = ( - (fixInfo.lineNumber !== lastFixInfo.lineNumber) || - (fixInfo.editColumn !== lastFixInfo.editColumn) || - (fixInfo.deleteCount !== lastFixInfo.deleteCount) || - (fixInfo.insertText !== lastFixInfo.insertText) - ); - lastFixInfo = fixInfo; - return unique; - }); - // Collapse insert/no-delete and no-insert/delete for same line/column - lastFixInfo = { - "lineNumber": -1 - }; - for (const fixInfo of fixInfos) { - if ( - (fixInfo.lineNumber === lastFixInfo.lineNumber) && - (fixInfo.editColumn === lastFixInfo.editColumn) && - !fixInfo.insertText && - (fixInfo.deleteCount > 0) && - lastFixInfo.insertText && - !lastFixInfo.deleteCount) { - fixInfo.insertText = lastFixInfo.insertText; - lastFixInfo.lineNumber = 0; - } - lastFixInfo = fixInfo; - } - fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber); - // Apply all (remaining/updated) fixes - let lastLineIndex = -1; - let lastEditIndex = -1; - for (const fixInfo of fixInfos) { - const { lineNumber, editColumn, deleteCount } = fixInfo; - const lineIndex = lineNumber - 1; - const editIndex = editColumn - 1; - if ( - (lineIndex !== lastLineIndex) || - (deleteCount === -1) || - ((editIndex + deleteCount) <= - (lastEditIndex - ((deleteCount > 0) ? 0 : 1))) - ) { - // @ts-ignore - lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding); - } - lastLineIndex = lineIndex; - lastEditIndex = editIndex; - } - // Return corrected input - return lines.filter((line) => line !== null).join(lineEnding); -} -module.exports.applyFixes = applyFixes; - /** * Expands a path with a tilde to an absolute path. * diff --git a/lib/markdownlint.d.ts b/lib/markdownlint.d.ts index f7bbb380..d12abe6b 100644 --- a/lib/markdownlint.d.ts +++ b/lib/markdownlint.d.ts @@ -8,7 +8,7 @@ export = markdownlint; */ declare function markdownlint(options: Options | null, callback: LintCallback): void; declare namespace markdownlint { - export { markdownlintSync as sync, readConfig, readConfigSync, getVersion, promises, RuleFunction, RuleParams, MarkdownParsers, ParserMarkdownIt, ParserMicromark, MarkdownItToken, MicromarkTokenType, MicromarkToken, RuleOnError, RuleOnErrorInfo, RuleOnErrorFixInfo, Rule, Options, Plugin, ToStringCallback, LintResults, LintError, FixInfo, LintContentCallback, LintCallback, Configuration, ConfigurationStrict, RuleConfiguration, ConfigurationParser, ReadConfigCallback, ResolveConfigExtendsCallback }; + export { markdownlintSync as sync, readConfig, readConfigSync, getVersion, promises, applyFix, applyFixes, RuleFunction, RuleParams, MarkdownParsers, ParserMarkdownIt, ParserMicromark, MarkdownItToken, MicromarkTokenType, MicromarkToken, RuleOnError, RuleOnErrorInfo, RuleOnErrorFixInfo, RuleOnErrorFixInfoNormalized, Rule, Options, Plugin, ToStringCallback, LintResults, LintError, FixInfo, LintContentCallback, LintCallback, Configuration, ConfigurationStrict, RuleConfiguration, ConfigurationParser, ReadConfigCallback, ResolveConfigExtendsCallback }; } /** * Lint specified Markdown files synchronously. @@ -49,6 +49,23 @@ declare namespace promises { export { extendConfigPromise as extendConfig }; export { readConfigPromise as readConfig }; } +/** + * Applies the specified fix to a Markdown content line. + * + * @param {string} line Line of Markdown content. + * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {string} [lineEnding] Line ending to use. + * @returns {string | null} Fixed content or null if deleted. + */ +declare function applyFix(line: string, fixInfo: RuleOnErrorFixInfo, lineEnding?: string): string | null; +/** + * Applies as many of the specified fixes as possible to Markdown content. + * + * @param {string} input Lines of Markdown content. + * @param {RuleOnErrorInfo[]} errors RuleOnErrorInfo instances. + * @returns {string} Fixed content. + */ +declare function applyFixes(input: string, errors: RuleOnErrorInfo[]): string; /** * Function to implement rule logic. */ @@ -270,6 +287,27 @@ type RuleOnErrorFixInfo = { */ insertText?: string; }; +/** + * RuleOnErrorInfo with all optional properties present. + */ +type RuleOnErrorFixInfoNormalized = { + /** + * Line number (1-based). + */ + lineNumber: number; + /** + * Column of the fix (1-based). + */ + editColumn: number; + /** + * Count of characters to delete. + */ + deleteCount: number; + /** + * Text to insert (after deleting). + */ + insertText: string; +}; /** * Rule definition. */ diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 92f6cb56..435971e0 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -1204,6 +1204,119 @@ function readConfigSync(file, parsers, fs) { return config; } +/** + * Normalizes the fields of a RuleOnErrorFixInfo instance. + * + * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {number} [lineNumber] Line number. + * @returns {RuleOnErrorFixInfoNormalized} Normalized RuleOnErrorFixInfo instance. + */ +function normalizeFixInfo(fixInfo, lineNumber = 0) { + return { + "lineNumber": fixInfo.lineNumber || lineNumber, + "editColumn": fixInfo.editColumn || 1, + "deleteCount": fixInfo.deleteCount || 0, + "insertText": fixInfo.insertText || "" + }; +} + +/** + * Applies the specified fix to a Markdown content line. + * + * @param {string} line Line of Markdown content. + * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {string} [lineEnding] Line ending to use. + * @returns {string | null} Fixed content or null if deleted. + */ +function applyFix(line, fixInfo, lineEnding = "\n") { + const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo); + const editIndex = editColumn - 1; + return (deleteCount === -1) ? + null : + line.slice(0, editIndex) + insertText.replace(/\n/g, lineEnding) + line.slice(editIndex + deleteCount); +} + +/** + * Applies as many of the specified fixes as possible to Markdown content. + * + * @param {string} input Lines of Markdown content. + * @param {RuleOnErrorInfo[]} errors RuleOnErrorInfo instances. + * @returns {string} Fixed content. + */ +function applyFixes(input, errors) { + const lineEnding = helpers.getPreferredLineEnding(input, require("node:os")); + const lines = input.split(helpers.newLineRe); + // Normalize fixInfo objects + let fixInfos = errors + .filter((error) => error.fixInfo) + // @ts-ignore + .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber)); + // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short + fixInfos.sort((a, b) => { + const aDeletingLine = (a.deleteCount === -1); + const bDeletingLine = (b.deleteCount === -1); + return ( + (b.lineNumber - a.lineNumber) || + (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) || + (b.editColumn - a.editColumn) || + (b.insertText.length - a.insertText.length) + ); + }); + // Remove duplicate entries (needed for following collapse step) + // eslint-disable-next-line jsdoc/valid-types + /** @type RuleOnErrorFixInfo */ + let lastFixInfo = {}; + fixInfos = fixInfos.filter((fixInfo) => { + const unique = ( + (fixInfo.lineNumber !== lastFixInfo.lineNumber) || + (fixInfo.editColumn !== lastFixInfo.editColumn) || + (fixInfo.deleteCount !== lastFixInfo.deleteCount) || + (fixInfo.insertText !== lastFixInfo.insertText) + ); + lastFixInfo = fixInfo; + return unique; + }); + // Collapse insert/no-delete and no-insert/delete for same line/column + lastFixInfo = { + "lineNumber": -1 + }; + for (const fixInfo of fixInfos) { + if ( + (fixInfo.lineNumber === lastFixInfo.lineNumber) && + (fixInfo.editColumn === lastFixInfo.editColumn) && + !fixInfo.insertText && + (fixInfo.deleteCount > 0) && + lastFixInfo.insertText && + !lastFixInfo.deleteCount) { + fixInfo.insertText = lastFixInfo.insertText; + lastFixInfo.lineNumber = 0; + } + lastFixInfo = fixInfo; + } + fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber); + // Apply all (remaining/updated) fixes + let lastLineIndex = -1; + let lastEditIndex = -1; + for (const fixInfo of fixInfos) { + const { lineNumber, editColumn, deleteCount } = fixInfo; + const lineIndex = lineNumber - 1; + const editIndex = editColumn - 1; + if ( + (lineIndex !== lastLineIndex) || + (deleteCount === -1) || + ((editIndex + deleteCount) <= + (lastEditIndex - ((deleteCount > 0) ? 0 : 1))) + ) { + // @ts-ignore + lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding); + } + lastLineIndex = lineIndex; + lastEditIndex = editIndex; + } + // Return corrected input + return lines.filter((line) => line !== null).join(lineEnding); +} + /** * Gets the (semantic) version of the library. * @@ -1223,6 +1336,8 @@ markdownlint.promises = { "extendConfig": extendConfigPromise, "readConfig": readConfigPromise }; +markdownlint.applyFix = applyFix; +markdownlint.applyFixes = applyFixes; module.exports = markdownlint; // Type declarations @@ -1341,6 +1456,16 @@ module.exports = markdownlint; * @property {string} [insertText] Text to insert (after deleting). */ +/** + * RuleOnErrorInfo with all optional properties present. + * + * @typedef {Object} RuleOnErrorFixInfoNormalized + * @property {number} lineNumber Line number (1-based). + * @property {number} editColumn Column of the fix (1-based). + * @property {number} deleteCount Count of characters to delete. + * @property {string} insertText Text to insert (after deleting). + */ + /** * Rule definition. * diff --git a/package.json b/package.json index 2729df95..a81d2adb 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "lint-test-repos": "ava --timeout=10m test/markdownlint-test-repos-*.js", "serial-config-docs": "npm run build-config && npm run build-docs", "serial-declaration-demo": "npm run build-declaration && npm-run-all --continue-on-error --parallel build-demo test-declaration", - "test": "ava --timeout=30s test/markdownlint-test.js test/markdownlint-test-config.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-micromark.mjs test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js helpers/test.cjs", + "test": "ava --timeout=30s test/markdownlint-test.js test/markdownlint-test-config.js test/markdownlint-test-custom-rules.js test/markdownlint-test-fixes.js test/markdownlint-test-helpers.js test/markdownlint-test-micromark.mjs test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js helpers/test.cjs", "test-cover": "c8 --100 npm test", "test-declaration": "cd example/typescript && tsc --module nodenext && tsc --module commonjs && node type-check.js", "test-extra": "ava --timeout=10m test/markdownlint-test-extra-parse.js test/markdownlint-test-extra-type.js", diff --git a/test/markdownlint-test-fixes.js b/test/markdownlint-test-fixes.js new file mode 100644 index 00000000..94ef5f7e --- /dev/null +++ b/test/markdownlint-test-fixes.js @@ -0,0 +1,532 @@ +// @ts-check + +"use strict"; + +const test = require("ava").default; +const markdownlint = require("../lib/markdownlint"); + +test("applyFix", (t) => { + t.plan(4); + const testCases = [ + [ + "Hello world.", + { + "editColumn": 12, + "deleteCount": 1 + }, + undefined, + "Hello world" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + undefined, + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\n", + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\r\n", + "Hello world.\r\n" + ] + ]; + for (const testCase of testCases) { + const [ line, fixInfo, lineEnding, expected ] = testCase; + // @ts-ignore + const actual = markdownlint.applyFix(line, fixInfo, lineEnding); + t.is(actual, String(expected), "Incorrect fix applied."); + } +}); + +test("applyFixes", (t) => { + t.plan(30); + const testCases = [ + [ + "Hello world.", + [], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": {} + } + ], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "Very " + } + } + ], + "Very Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "insertText": "big " + } + } + ], + "Hello big world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": 6 + } + } + ], + "world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "there" + } + } + ], + "Hello there." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 12, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 6, + "deleteCount": 1 + } + } + ], + "Helloworld" + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 13, + "insertText": " Hi." + } + } + ], + "Hello world. Hi." + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "Big " + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "aa" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "b" + } + } + ], + "aaHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "a" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "bb" + } + } + ], + "bbHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 6, + "insertText": " big" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello big orld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello zorld" + ], + [ + "Hello\nworld\nhello\rworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\nworld\nhello\nworld\n" + ], + [ + "Hello\r\nworld\r\nhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\r\nworld\r\nhello\r\nworld\r\n" + ], + [ + "Hello\rworld\rhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\rworld\rhello\rworld\r" + ], + [ + "Hello\r\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "editColumn": 6, + "insertText": "\n\n" + } + } + ], + "Hello\r\nworld\r\n\r\n" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "x" + } + }, + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "" + ], + [ + " hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 1, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 2, + "deleteCount": 1, + "insertText": "H" + } + } + ], + "Hello world" + ] + ]; + for (const testCase of testCases) { + const [ input, errors, expected ] = testCase; + // @ts-ignore + const actual = markdownlint.applyFixes(input, errors); + t.is(actual, String(expected), "Incorrect fix applied."); + } +}); diff --git a/test/markdownlint-test-helpers.js b/test/markdownlint-test-helpers.js index e1f2bb83..96e6594d 100644 --- a/test/markdownlint-test-helpers.js +++ b/test/markdownlint-test-helpers.js @@ -6,7 +6,8 @@ const os = require("node:os"); const path = require("node:path"); const test = require("ava").default; const helpers = require("../helpers"); -const { markdownlint } = require("../lib/markdownlint").promises; +const libMarkdownlint = require("../lib/markdownlint"); +const { markdownlint } = libMarkdownlint.promises; const { forEachInlineCodeSpan } = require("../lib/markdownit.cjs"); test("clearHtmlCommentTextValid", (t) => { @@ -352,532 +353,6 @@ test("getPreferredLineEnding", (t) => { t.is(helpers.getPreferredLineEnding("", { "EOL": "custom" }), "custom"); }); -test("applyFix", (t) => { - t.plan(4); - const testCases = [ - [ - "Hello world.", - { - "editColumn": 12, - "deleteCount": 1 - }, - undefined, - "Hello world" - ], - [ - "Hello world.", - { - "editColumn": 13, - "insertText": "\n" - }, - undefined, - "Hello world.\n" - ], - [ - "Hello world.", - { - "editColumn": 13, - "insertText": "\n" - }, - "\n", - "Hello world.\n" - ], - [ - "Hello world.", - { - "editColumn": 13, - "insertText": "\n" - }, - "\r\n", - "Hello world.\r\n" - ] - ]; - for (const testCase of testCases) { - const [ line, fixInfo, lineEnding, expected ] = testCase; - // @ts-ignore - const actual = helpers.applyFix(line, fixInfo, lineEnding); - t.is(actual, String(expected), "Incorrect fix applied."); - } -}); - -test("applyFixes", (t) => { - t.plan(30); - const testCases = [ - [ - "Hello world.", - [], - "Hello world." - ], - [ - "Hello world.", - [ - { - "lineNumber": 1, - "fixInfo": {} - } - ], - "Hello world." - ], - [ - "Hello world.", - [ - { - "lineNumber": 1, - "fixInfo": { - "insertText": "Very " - } - } - ], - "Very Hello world." - ], - [ - "Hello world.", - [ - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 7, - "insertText": "big " - } - } - ], - "Hello big world." - ], - [ - "Hello world.", - [ - { - "lineNumber": 1, - "fixInfo": { - "deleteCount": 6 - } - } - ], - "world." - ], - [ - "Hello world.", - [ - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 7, - "deleteCount": 5, - "insertText": "there" - } - } - ], - "Hello there." - ], - [ - "Hello world.", - [ - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 12, - "deleteCount": 1 - } - }, - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 6, - "deleteCount": 1 - } - } - ], - "Helloworld" - ], - [ - "Hello world.", - [ - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 13, - "insertText": " Hi." - } - } - ], - "Hello world. Hi." - ], - [ - "Hello\nworld", - [ - { - "lineNumber": 1, - "fixInfo": { - "deleteCount": -1 - } - } - ], - "world" - ], - [ - "Hello\nworld", - [ - { - "lineNumber": 2, - "fixInfo": { - "deleteCount": -1 - } - } - ], - "Hello" - ], - [ - "Hello\nworld", - [ - { - "lineNumber": 2, - "fixInfo": { - "lineNumber": 1, - "deleteCount": -1 - } - } - ], - "world" - ], - [ - "Hello\nworld", - [ - { - "lineNumber": 1, - "fixInfo": { - "lineNumber": 2, - "deleteCount": -1 - } - } - ], - "Hello" - ], - [ - "Hello world", - [ - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 4, - "deleteCount": 1 - } - }, - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 10, - "deleteCount": 1 - } - } - ], - "Helo word" - ], - [ - "Hello world", - [ - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 10, - "deleteCount": 1 - } - }, - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 4, - "deleteCount": 1 - } - } - ], - "Helo word" - ], - [ - "Hello\nworld", - [ - { - "fixInfo": { - "lineNumber": 1, - "deleteCount": -1 - } - }, - { - "fixInfo": { - "lineNumber": 1, - "insertText": "Big " - } - } - ], - "world" - ], - [ - "Hello\nworld", - [ - { - "fixInfo": { - "lineNumber": 1, - "deleteCount": -1 - } - }, - { - "fixInfo": { - "lineNumber": 2, - "deleteCount": -1 - } - } - ], - "" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "insertText": "aa" - } - }, - { - "fixInfo": { - "lineNumber": 1, - "insertText": "b" - } - } - ], - "aaHello world" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "insertText": "a" - } - }, - { - "fixInfo": { - "lineNumber": 1, - "insertText": "bb" - } - } - ], - "bbHello world" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 6, - "insertText": " big" - } - }, - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "deleteCount": 1 - } - } - ], - "Hello big orld" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 8, - "deleteCount": 2 - } - }, - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "deleteCount": 2 - } - } - ], - "Hello wld" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "deleteCount": 2 - } - }, - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 8, - "deleteCount": 2 - } - } - ], - "Hello wld" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "deleteCount": 1, - "insertText": "z" - } - } - ], - "Hello zorld" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "deleteCount": 1 - } - }, - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "insertText": "z" - } - } - ], - "Hello zorld" - ], - [ - "Hello world", - [ - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "insertText": "z" - } - }, - { - "fixInfo": { - "lineNumber": 1, - "editColumn": 7, - "deleteCount": 1 - } - } - ], - "Hello zorld" - ], - [ - "Hello\nworld\nhello\rworld", - [ - { - "fixInfo": { - "lineNumber": 4, - "editColumn": 6, - "insertText": "\n" - } - } - ], - "Hello\nworld\nhello\nworld\n" - ], - [ - "Hello\r\nworld\r\nhello\nworld", - [ - { - "fixInfo": { - "lineNumber": 4, - "editColumn": 6, - "insertText": "\n" - } - } - ], - "Hello\r\nworld\r\nhello\r\nworld\r\n" - ], - [ - "Hello\rworld\rhello\nworld", - [ - { - "fixInfo": { - "lineNumber": 4, - "editColumn": 6, - "insertText": "\n" - } - } - ], - "Hello\rworld\rhello\rworld\r" - ], - [ - "Hello\r\nworld", - [ - { - "lineNumber": 2, - "fixInfo": { - "editColumn": 6, - "insertText": "\n\n" - } - } - ], - "Hello\r\nworld\r\n\r\n" - ], - [ - "Hello world", - [ - { - "lineNumber": 1, - "fixInfo": { - "insertText": "x" - } - }, - { - "lineNumber": 1, - "fixInfo": { - "deleteCount": -1 - } - } - ], - "" - ], - [ - " hello world", - [ - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 1, - "deleteCount": 1 - } - }, - { - "lineNumber": 1, - "fixInfo": { - "editColumn": 2, - "deleteCount": 1, - "insertText": "H" - } - } - ], - "Hello world" - ] - ]; - for (const testCase of testCases) { - const [ input, errors, expected ] = testCase; - // @ts-ignore - const actual = helpers.applyFixes(input, errors); - t.is(actual, String(expected), "Incorrect fix applied."); - } -}); - test("expandTildePath", (t) => { t.plan(17); const homedir = os.homedir(); diff --git a/test/markdownlint-test-scenarios.js b/test/markdownlint-test-scenarios.js index 6159ddd5..ae563db5 100644 --- a/test/markdownlint-test-scenarios.js +++ b/test/markdownlint-test-scenarios.js @@ -5,7 +5,9 @@ const fs = require("node:fs").promises; const path = require("node:path"); const test = require("ava").default; -const { markdownlint } = require("../lib/markdownlint").promises; +const libMarkdownlint = require("../lib/markdownlint"); +const { applyFixes, promises } = libMarkdownlint; +const { markdownlint } = promises; const helpers = require("../helpers"); const constants = require("../lib/constants"); @@ -82,7 +84,7 @@ function createTestForFile(file) { } t.deepEqual(actual, expected, "Too few or too many issues found."); // Create snapshot - const fixed = helpers.applyFixes(content, errors) + const fixed = applyFixes(content, errors) .replace(/\r\n/g, "\n"); t.snapshot({ errors, diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 6cf7ebb9..da65b118 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -686,8 +686,8 @@ test("readmeHeadings", (t) => new Promise((resolve) => { "#### fs", "#### callback", "#### result", - "## Usage", "### Fixing", + "## Usage", "## Browser", "## Examples", "## Contributing",