From 616c3f2c2299ce590f32290452868b9202ec22a5 Mon Sep 17 00:00:00 2001 From: David Anson Date: Mon, 8 Sep 2025 21:09:35 -0700 Subject: [PATCH] Deprecate LintResults.toString() (at edit time; continue to support at run time), provide helper function formatLintResults, remove outdated grunt/gulp samples, improve typing. --- README.md | 49 +++++--------- example/Gruntfile.cjs | 28 -------- example/gulpfile.cjs | 24 ------- example/standalone.mjs | 10 +-- example/typescript/type-check.ts | 25 ++++--- helpers/helpers.cjs | 29 +++++++++ lib/exports.d.mts | 3 +- lib/exports.mjs | 3 +- lib/markdownlint.d.mts | 58 ++++++++--------- lib/markdownlint.mjs | 42 +++++------- package.json | 3 +- test/markdownlint-test-helpers.mjs | 16 ++++- test/markdownlint-test-repos.mjs | 4 +- test/markdownlint-test.mjs | 61 ++++++++---------- .../markdownlint-test-exports.mjs.md | 1 + .../markdownlint-test-exports.mjs.snap | Bin 1544 -> 1564 bytes .../markdownlint-test-repos-small.mjs.md | 12 ++-- .../markdownlint-test-repos-small.mjs.snap | Bin 8239 -> 8243 bytes 18 files changed, 161 insertions(+), 207 deletions(-) delete mode 100644 example/Gruntfile.cjs delete mode 100644 example/gulpfile.cjs diff --git a/README.md b/README.md index 2e2c7916..74803df3 100644 --- a/README.md +++ b/README.md @@ -659,9 +659,9 @@ Standard completion callback. Type: `Object` -Call `result.toString()` for convenience or see below for an example of the -structure of the `result` object. Passing the value `true` to `toString()` -uses rule aliases (ex: `no-hard-tabs`) instead of names (ex: `MD010`). +Map of input file names and string identifiers to issues within. + +See the [Usage section](#usage) for an example of the structure of this object. ### Config @@ -837,7 +837,7 @@ console.log(getVersion()); ## Usage -Invoke `lint` and use the `result` object's `toString` method: +Invoke `lint` as an asynchronous call: ```javascript import { lint as lintAsync } from "markdownlint/async"; @@ -852,34 +852,21 @@ const options = { lintAsync(options, function callback(error, results) { if (!error && results) { - console.log(results.toString()); + console.dir(results, { "colors": true, "depth": null }); } }); ``` -Output: - -```text -bad.string: 3: MD010/no-hard-tabs Hard tabs [Column: 19] -bad.string: 1: MD018/no-missing-space-atx No space after hash on atx style heading [Context: "#bad.string"] -bad.string: 3: MD018/no-missing-space-atx No space after hash on atx style heading [Context: "#This string fails some rules."] -bad.string: 1: MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "#bad.string"] -bad.md: 3: MD010/no-hard-tabs Hard tabs [Column: 17] -bad.md: 1: MD018/no-missing-space-atx No space after hash on atx style heading [Context: "#bad.md"] -bad.md: 3: MD018/no-missing-space-atx No space after hash on atx style heading [Context: "#This file fails some rules."] -bad.md: 1: MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "#bad.md"] -``` - Or as a synchronous call: ```javascript import { lint as lintSync } from "markdownlint/sync"; const results = lintSync(options); -console.log(results.toString()); +console.dir(results, { "colors": true, "depth": null }); ``` -To examine the `result` object directly via a `Promise`-based call: +Or as a `Promise`-based call: ```javascript import { lint as lintPromise } from "markdownlint/promise"; @@ -888,7 +875,7 @@ const results = await lintPromise(options); console.dir(results, { "colors": true, "depth": null }); ``` -Output: +All of which return an object like: ```json { @@ -900,38 +887,36 @@ Output: "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md010.md", "errorDetail": "Column: 17", "errorContext": null, - "errorRange": [ 17, 1 ] }, + "errorRange": [ 17, 1 ], + "fixInfo": { "editColumn": 17, "deleteCount": 1, "insertText": ' ' } } { "lineNumber": 1, "ruleNames": [ "MD018", "no-missing-space-atx" ], "ruleDescription": "No space after hash on atx style heading", "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md018.md", "errorDetail": null, "errorContext": "#bad.md", - "errorRange": [ 1, 2 ] }, + "errorRange": [ 1, 2 ], + "fixInfo": { "editColumn": 2, "insertText": ' ' } } { "lineNumber": 3, "ruleNames": [ "MD018", "no-missing-space-atx" ], "ruleDescription": "No space after hash on atx style heading", "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md018.md", "errorDetail": null, "errorContext": "#This file fails\tsome rules.", - "errorRange": [ 1, 2 ] }, + "errorRange": [ 1, 2 ], + "fixInfo": { "editColumn": 2, "insertText": ' ' } } { "lineNumber": 1, "ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ], "ruleDescription": "First line in a file should be a top-level heading", "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md041.md", "errorDetail": null, "errorContext": "#bad.md", - "errorRange": null } + "errorRange": null, + "fixInfo": null } ] } ``` -Integration with the [gulp](https://gulpjs.com/) build system is -straightforward: [`gulpfile.cjs`](example/gulpfile.cjs). - -Integration with the [Grunt](https://gruntjs.com/) build system is similar: -[`Gruntfile.cjs`](example/Gruntfile.cjs). - ## Browser `markdownlint` also works in the browser. @@ -957,7 +942,7 @@ const options = { } }; -const results = globalThis.markdownlint.lintSync(options).toString(); +const results = globalThis.markdownlint.lintSync(options); ``` ## Examples diff --git a/example/Gruntfile.cjs b/example/Gruntfile.cjs deleted file mode 100644 index 571d403d..00000000 --- a/example/Gruntfile.cjs +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-check - -"use strict"; - -module.exports = function wrapper(grunt) { - grunt.initConfig({ - "markdownlint": { - "example": { - "src": [ "*.md" ] - } - } - }); - - grunt.registerMultiTask("markdownlint", function task() { - const done = this.async(); - import("markdownlint/async").then(({ lint }) => { - lint( - { "files": this.filesSrc }, - function callback(err, result) { - const resultString = err || ((result || "").toString()); - if (resultString) { - grunt.fail.warn("\n" + resultString + "\n"); - } - done(!err || !resultString); - }); - }).catch(done); - }); -}; diff --git a/example/gulpfile.cjs b/example/gulpfile.cjs deleted file mode 100644 index 25dcdd58..00000000 --- a/example/gulpfile.cjs +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-check - -"use strict"; - -const gulp = require("gulp"); -const through2 = require("through2"); - -// Simple task wrapper -gulp.task("markdownlint", function task() { - return gulp.src("*.md", { "read": false }) - .pipe(through2.obj(function obj(file, enc, next) { - import("markdownlint/async").then(({ lint }) => { - lint( - { "files": [ file.relative ] }, - function callback(err, result) { - const resultString = (result || "").toString(); - if (resultString) { - console.log(resultString); - } - next(err, file); - }); - }).catch(next); - })); -}); diff --git a/example/standalone.mjs b/example/standalone.mjs index 2614e14b..866d0a8b 100644 --- a/example/standalone.mjs +++ b/example/standalone.mjs @@ -18,18 +18,18 @@ const options = { if (true) { - // Makes a synchronous call, uses result.toString for pretty formatting + // Makes a synchronous call const results = lintSync(options); - console.log(results.toString()); + console.dir(results, { "colors": true, "depth": null }); } if (true) { - // Makes an asynchronous call, uses result.toString for pretty formatting + // Makes an asynchronous call lintAsync(options, function callback(error, results) { if (!error && results) { - console.log(results.toString()); + console.dir(results, { "colors": true, "depth": null }); } }); @@ -37,7 +37,7 @@ if (true) { if (true) { - // Makes a Promise-based asynchronous call, displays the result object directly + // Makes a Promise-based asynchronous call const results = await lintPromise(options); console.dir(results, { "colors": true, "depth": null }); diff --git a/example/typescript/type-check.ts b/example/typescript/type-check.ts index 5abc47f0..4bc3e04c 100644 --- a/example/typescript/type-check.ts +++ b/example/typescript/type-check.ts @@ -171,13 +171,15 @@ lintAsync(options, assertLintResultsCallback); assertLintResultsCallback(null, await lintPromise(options)); })(); +const needsFixing = "# Heading\n"; + assert.equal( applyFix( - "# Fixing\n", + needsFixing, { - "insertText": "Head", - "editColumn": 3, - "deleteCount": 3 + "insertText": " ", + "editColumn": 2, + "deleteCount": 2 }, "\n" ), @@ -186,17 +188,12 @@ assert.equal( assert.equal( applyFixes( - "# Fixing\n", - [ - { - "lineNumber": 1, - "fixInfo": { - "insertText": "Head", - "editColumn": 3, - "deleteCount": 3 - } + needsFixing, + lintSync({ + "strings": { + needsFixing } - ] + })["needsFixing"] ), "# Heading\n" ); diff --git a/helpers/helpers.cjs b/helpers/helpers.cjs index 97e97f7f..20875652 100644 --- a/helpers/helpers.cjs +++ b/helpers/helpers.cjs @@ -554,6 +554,7 @@ function convertLintErrorsVersion3To2(errors) { "lineNumber": -1 }; return errors.filter((error, index, array) => { + // @ts-ignore delete error.fixInfo; const previous = array[index - 1] || noPrevious; return ( @@ -646,3 +647,31 @@ module.exports.convertToResultVersion1 = function convertToResultVersion1(result module.exports.convertToResultVersion2 = function convertToResultVersion2(results) { return copyAndTransformResults(results, convertLintErrorsVersion3To2); }; + +/** + * Formats lint results to an array of strings. + * + * @param {LintResults|undefined} lintResults Lint results. + * @returns {string[]} Lint error strings. + */ +module.exports.formatLintResults = function formatLintResults(lintResults) { + const results = []; + const entries = Object.entries(lintResults || {}); + entries.sort((a, b) => a[0].localeCompare(b[0])); + for (const [ source, lintErrors ] of entries) { + for (const lintError of lintErrors) { + results.push( + source + ": " + + lintError.lineNumber + ": " + + lintError.ruleNames.join("/") + " " + + lintError.ruleDescription + + (lintError.errorDetail ? + " [" + lintError.errorDetail + "]" : + "") + + (lintError.errorContext ? + " [Context: \"" + lintError.errorContext + "\"]" : + "")); + } + } + return results; +}; diff --git a/lib/exports.d.mts b/lib/exports.d.mts index 7d9639af..64f50b53 100644 --- a/lib/exports.d.mts +++ b/lib/exports.d.mts @@ -3,6 +3,7 @@ export type Configuration = import("./markdownlint.mjs").Configuration; export type ConfigurationParser = import("./markdownlint.mjs").ConfigurationParser; export type ConfigurationStrict = import("./markdownlint.mjs").ConfigurationStrict; export type FixInfo = import("./markdownlint.mjs").FixInfo; +export type FixInfoNormalized = import("./markdownlint.mjs").FixInfoNormalized; export type LintCallback = import("./markdownlint.mjs").LintCallback; export type LintContentCallback = import("./markdownlint.mjs").LintContentCallback; export type LintError = import("./markdownlint.mjs").LintError; @@ -23,8 +24,6 @@ export type RuleConfiguration = import("./markdownlint.mjs").RuleConfiguration; export type RuleFunction = import("./markdownlint.mjs").RuleFunction; export type RuleOnError = import("./markdownlint.mjs").RuleOnError; export type RuleOnErrorFixInfo = import("./markdownlint.mjs").RuleOnErrorFixInfo; -export type RuleOnErrorFixInfoNormalized = import("./markdownlint.mjs").RuleOnErrorFixInfoNormalized; export type RuleOnErrorInfo = import("./markdownlint.mjs").RuleOnErrorInfo; export type RuleParams = import("./markdownlint.mjs").RuleParams; -export type ToStringCallback = import("./markdownlint.mjs").ToStringCallback; export { applyFix, applyFixes, getVersion } from "./markdownlint.mjs"; diff --git a/lib/exports.mjs b/lib/exports.mjs index dc0d137d..b27030d4 100644 --- a/lib/exports.mjs +++ b/lib/exports.mjs @@ -7,6 +7,7 @@ export { resolveModule } from "./resolve-module.cjs"; /** @typedef {import("./markdownlint.mjs").ConfigurationParser} ConfigurationParser */ /** @typedef {import("./markdownlint.mjs").ConfigurationStrict} ConfigurationStrict */ /** @typedef {import("./markdownlint.mjs").FixInfo} FixInfo */ +/** @typedef {import("./markdownlint.mjs").FixInfoNormalized} FixInfoNormalized */ /** @typedef {import("./markdownlint.mjs").LintCallback} LintCallback */ /** @typedef {import("./markdownlint.mjs").LintContentCallback} LintContentCallback */ /** @typedef {import("./markdownlint.mjs").LintError} LintError */ @@ -27,7 +28,5 @@ export { resolveModule } from "./resolve-module.cjs"; /** @typedef {import("./markdownlint.mjs").RuleFunction} RuleFunction */ /** @typedef {import("./markdownlint.mjs").RuleOnError} RuleOnError */ /** @typedef {import("./markdownlint.mjs").RuleOnErrorFixInfo} RuleOnErrorFixInfo */ -/** @typedef {import("./markdownlint.mjs").RuleOnErrorFixInfoNormalized} RuleOnErrorFixInfoNormalized */ /** @typedef {import("./markdownlint.mjs").RuleOnErrorInfo} RuleOnErrorInfo */ /** @typedef {import("./markdownlint.mjs").RuleParams} RuleParams */ -/** @typedef {import("./markdownlint.mjs").ToStringCallback} ToStringCallback */ diff --git a/lib/markdownlint.d.mts b/lib/markdownlint.d.mts index 331262e9..98550b53 100644 --- a/lib/markdownlint.d.mts +++ b/lib/markdownlint.d.mts @@ -63,19 +63,19 @@ export function readConfigSync(file: string, parsers?: ConfigurationParser[], fs * Applies the specified fix to a Markdown content line. * * @param {string} line Line of Markdown content. - * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {FixInfo} fixInfo FixInfo instance. * @param {string} [lineEnding] Line ending to use. * @returns {string | null} Fixed content or null if deleted. */ -export function applyFix(line: string, fixInfo: RuleOnErrorFixInfo, lineEnding?: string): string | null; +export function applyFix(line: string, fixInfo: FixInfo, 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. + * @param {LintError[]} errors LintError instances. * @returns {string} Fixed content. */ -export function applyFixes(input: string, errors: RuleOnErrorInfo[]): string; +export function applyFixes(input: string, errors: LintError[]): string; /** * Gets the (semantic) version of the library. * @@ -320,27 +320,6 @@ export type RuleOnErrorFixInfo = { */ insertText?: string; }; -/** - * RuleOnErrorInfo with all optional properties present. - */ -export 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. */ @@ -442,10 +421,6 @@ export type Options = { * A markdown-it plugin. */ export type Plugin = any[]; -/** - * Function to pretty-print lint results. - */ -export type ToStringCallback = (ruleAliases?: boolean) => string; /** * Lint results. */ @@ -483,11 +458,11 @@ export type LintError = { /** * Column number (1-based) and length. */ - errorRange: number[]; + errorRange: number[] | null; /** * Fix information. */ - fixInfo?: FixInfo; + fixInfo: FixInfo | null; }; /** * Fix information. @@ -510,6 +485,27 @@ export type FixInfo = { */ insertText?: string; }; +/** + * FixInfo with all optional properties present. + */ +export type FixInfoNormalized = { + /** + * 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; +}; /** * Called with the result of linting a string or document. */ diff --git a/lib/markdownlint.mjs b/lib/markdownlint.mjs index b3d8f25f..73194b20 100644 --- a/lib/markdownlint.mjs +++ b/lib/markdownlint.mjs @@ -522,6 +522,7 @@ function lintContent( "config": null }); // Function to run for each rule + /** @type {LintError[]} */ const results = []; /** * @param {Rule} rule Rule. @@ -1194,9 +1195,9 @@ export function readConfigSync(file, parsers, fs) { /** * Normalizes the fields of a RuleOnErrorFixInfo instance. * - * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {FixInfo} fixInfo RuleOnErrorFixInfo instance. * @param {number} [lineNumber] Line number. - * @returns {RuleOnErrorFixInfoNormalized} Normalized RuleOnErrorFixInfo instance. + * @returns {FixInfoNormalized} Normalized RuleOnErrorFixInfo instance. */ function normalizeFixInfo(fixInfo, lineNumber = 0) { return { @@ -1211,7 +1212,7 @@ function normalizeFixInfo(fixInfo, lineNumber = 0) { * Applies the specified fix to a Markdown content line. * * @param {string} line Line of Markdown content. - * @param {RuleOnErrorFixInfo} fixInfo RuleOnErrorFixInfo instance. + * @param {FixInfo} fixInfo FixInfo instance. * @param {string} [lineEnding] Line ending to use. * @returns {string | null} Fixed content or null if deleted. */ @@ -1227,7 +1228,7 @@ export 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. + * @param {LintError[]} errors LintError instances. * @returns {string} Fixed content. */ export function applyFixes(input, errors) { @@ -1424,16 +1425,6 @@ export function getVersion() { * @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. * @@ -1492,19 +1483,10 @@ export function getVersion() { * @typedef {Array} Plugin */ -/** - * Function to pretty-print lint results. - * - * @callback ToStringCallback - * @param {boolean} [ruleAliases] True to use rule aliases. - * @returns {string} Pretty-printed results. - */ - /** * Lint results. * * @typedef {Object.} LintResults - * @property {ToStringCallback} toString String representation. */ /** @@ -1517,8 +1499,8 @@ export function getVersion() { * @property {string} ruleInformation Link to more information. * @property {string} errorDetail Detail about the error. * @property {string} errorContext Context for the error. - * @property {number[]} errorRange Column number (1-based) and length. - * @property {FixInfo} [fixInfo] Fix information. + * @property {number[]|null} errorRange Column number (1-based) and length. + * @property {FixInfo|null} fixInfo Fix information. */ /** @@ -1531,6 +1513,16 @@ export function getVersion() { * @property {string} [insertText] Text to insert (after deleting). */ +/** + * FixInfo with all optional properties present. + * + * @typedef {Object} FixInfoNormalized + * @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). + */ + /** * Called with the result of linting a string or document. * diff --git a/package.json b/package.json index bea19cdf..7bb06b08 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "build-declaration": "tsc --allowJs --checkJs --declaration --emitDeclarationOnly --module nodenext --outDir dts --rootDir . --target es2015 lib/exports.mjs lib/exports-async.mjs lib/exports-promise.mjs lib/exports-sync.mjs lib/markdownlint.mjs lib/resolve-module.cjs && node scripts/index.mjs copy dts/lib/exports.d.mts lib/exports.d.mts && node scripts/index.mjs copy dts/lib/exports-async.d.mts lib/exports-async.d.mts && node scripts/index.mjs copy dts/lib/exports-promise.d.mts lib/exports-promise.d.mts && node scripts/index.mjs copy dts/lib/exports-sync.d.mts lib/exports-sync.d.mts && node scripts/index.mjs copy dts/lib/markdownlint.d.mts lib/markdownlint.d.mts && node scripts/index.mjs copy dts/lib/resolve-module.d.cts lib/resolve-module.d.cts && node scripts/index.mjs remove dts", "build-demo": "node scripts/index.mjs copy node_modules/markdown-it/dist/markdown-it.min.js demo/markdown-it.min.js && cd demo && webpack --no-stats", "build-docs": "node doc-build/build-rules.mjs", - "build-example": "npm install --no-save --ignore-scripts grunt grunt-cli gulp through2", "ci": "npm-run-all --continue-on-error --parallel build-demo lint serial-config-docs serial-declaration test-cover && git diff --exit-code", "clone-test-repos-apache-airflow": "cd test-repos && git clone https://github.com/apache/airflow apache-airflow --depth 1 --no-tags --quiet", "clone-test-repos-dotnet-docs": "cd test-repos && git clone https://github.com/dotnet/docs dotnet-docs --depth 1 --no-tags --quiet", @@ -54,7 +53,7 @@ "clone-test-repos-webpack-webpack-js-org": "cd test-repos && git clone https://github.com/webpack/webpack.js.org webpack-webpack-js-org --depth 1 --no-tags --quiet", "clone-test-repos": "mkdir test-repos && cd test-repos && npm run clone-test-repos-apache-airflow && npm run clone-test-repos-dotnet-docs && npm run clone-test-repos-electron-electron && npm run clone-test-repos-eslint-eslint && npm run clone-test-repos-mdn-content && npm run clone-test-repos-mkdocs-mkdocs && npm run clone-test-repos-mochajs-mocha && npm run clone-test-repos-pi-hole-docs && npm run clone-test-repos-v8-v8-dev && npm run clone-test-repos-webhintio-hint && npm run clone-test-repos-webpack-webpack-js-org", "declaration": "npm run build-declaration && npm run test-declaration", - "example": "cd example && node standalone.mjs && grunt markdownlint --force && gulp markdownlint", + "example": "cd example && node standalone.mjs", "lint": "eslint --max-warnings 0", "lint-test-repos": "ava --timeout=10m test/markdownlint-test-repos-*.mjs", "serial-config-docs": "npm run build-config && npm run build-docs", diff --git a/test/markdownlint-test-helpers.mjs b/test/markdownlint-test-helpers.mjs index 64ff908f..3b6fe392 100644 --- a/test/markdownlint-test-helpers.mjs +++ b/test/markdownlint-test-helpers.mjs @@ -5,7 +5,7 @@ import path from "node:path"; import test from "ava"; import { characterEntities } from "character-entities"; import { gemoji } from "gemoji"; -import helpers from "../helpers/helpers.cjs"; +import helpers, { formatLintResults } from "../helpers/helpers.cjs"; import { lint } from "markdownlint/promise"; import { forEachInlineCodeSpan } from "../lib/markdownit.cjs"; import { getReferenceLinkImageData } from "../lib/cache.mjs"; @@ -529,3 +529,17 @@ test("hasOverlap", (t) => { t.false(helpers.hasOverlap(rangeB, rangeA), JSON.stringify({ rangeB, rangeA })); } }); + +test("formatLintResults", async(t) => { + t.plan(2); + t.deepEqual(formatLintResults(undefined), []); + const lintResults = await lint({ "strings": { "content": "# Heading
" } }); + t.deepEqual( + formatLintResults(lintResults), + [ + "content: 1: MD019/no-multiple-space-atx Multiple spaces after hash on atx style heading [Context: \"# Heading
\"]", + "content: 1: MD033/no-inline-html Inline HTML [Element: br]", + "content: 1: MD047/single-trailing-newline Files should end with a single newline character" + ] + ); +}); diff --git a/test/markdownlint-test-repos.mjs b/test/markdownlint-test-repos.mjs index d9b691ce..317835ca 100644 --- a/test/markdownlint-test-repos.mjs +++ b/test/markdownlint-test-repos.mjs @@ -5,6 +5,7 @@ const { join } = path.posix; import { globby } from "globby"; import jsoncParser from "jsonc-parser"; import jsYaml from "js-yaml"; +import { formatLintResults } from "markdownlint/helpers"; import { lint, readConfig } from "markdownlint/promise"; import { markdownlintParallel } from "./markdownlint-test-parallel.mjs"; @@ -49,9 +50,8 @@ export function lintTestRepo(t, globPatterns, configPath, configOverrides, paral files, config }).then((results) => { - const resultsString = results.toString(); t.snapshot( - resultsString, + formatLintResults(results).join("\n"), "Expected linting violations" ); }); diff --git a/test/markdownlint-test.mjs b/test/markdownlint-test.mjs index 06318fce..7345fab5 100644 --- a/test/markdownlint-test.mjs +++ b/test/markdownlint-test.mjs @@ -49,46 +49,42 @@ function getMarkdownItFactory(markdownItPlugins) { } test("simpleAsync", (t) => new Promise((resolve) => { + t.plan(3); + const options = { + "strings": { + "content": "# Heading" + } + }; + lintAsync(options, (err, actual) => { + t.falsy(err); + t.is(actual?.content.length, 1); + t.is(actual?.content[0].ruleNames[0], "MD047"); + resolve(); + }); +})); + +test("simpleSync", (t) => { t.plan(2); const options = { "strings": { "content": "# Heading" } }; - const expected = "content: 1: MD047/single-trailing-newline " + - "Files should end with a single newline character"; - lintAsync(options, (err, actual) => { - t.falsy(err); - // @ts-ignore - t.is(actual.toString(), expected, "Unexpected results."); - resolve(); - }); -})); - -test("simpleSync", (t) => { - t.plan(1); - const options = { - "strings": { - "content": "# Heading" - } - }; - const expected = "content: 1: MD047/single-trailing-newline " + - "Files should end with a single newline character"; - const actual = lintSync(options).toString(); - t.is(actual, expected, "Unexpected results."); + const actual = lintSync(options); + t.is(actual.content.length, 1); + t.is(actual.content[0].ruleNames[0], "MD047"); }); test("simplePromise", (t) => { - t.plan(1); + t.plan(2); const options = { "strings": { "content": "# Heading" } }; - const expected = "content: 1: MD047/single-trailing-newline " + - "Files should end with a single newline character"; return lintPromise(options).then((actual) => { - t.is(actual.toString(), expected, "Unexpected results."); + t.is(actual.content.length, 1); + t.is(actual.content[0].ruleNames[0], "MD047"); }); }); @@ -1281,7 +1277,7 @@ test("token-map-spans", (t) => { }); test("configParsersInvalid", async(t) => { - t.plan(1); + t.plan(2); const options = { "strings": { "content": [ @@ -1294,10 +1290,9 @@ test("configParsersInvalid", async(t) => { ].join("\n") } }; - const expected = "content: 1: MD041/first-line-heading/first-line-h1 " + - "First line in a file should be a top-level heading [Context: \"Text\"]"; const actual = await lintPromise(options); - t.is(actual.toString(), expected, "Unexpected results."); + t.is(actual.content.length, 1); + t.is(actual.content[0].ruleNames[0], "MD041"); }); test("configParsersJSON", async(t) => { @@ -1317,7 +1312,7 @@ test("configParsersJSON", async(t) => { } }; const actual = await lintPromise(options); - t.is(actual.toString(), "", "Unexpected results."); + t.is(actual.content.length, 0); }); test("configParsersJSONC", async(t) => { @@ -1339,7 +1334,7 @@ test("configParsersJSONC", async(t) => { "configParsers": [ jsoncParser.parse ] }; const actual = await lintPromise(options); - t.is(actual.toString(), "", "Unexpected results."); + t.is(actual.content.length, 0); }); test("configParsersYAML", async(t) => { @@ -1360,7 +1355,7 @@ test("configParsersYAML", async(t) => { }; // @ts-ignore const actual = await lintPromise(options); - t.is(actual.toString(), "", "Unexpected results."); + t.is(actual.content.length, 0); }); test("configParsersTOML", async(t) => { @@ -1382,7 +1377,7 @@ test("configParsersTOML", async(t) => { ] }; const actual = await lintPromise(options); - t.is(actual.toString(), "", "Unexpected results."); + t.is(actual.content.length, 0); }); test("getVersion", (t) => { diff --git a/test/snapshots/markdownlint-test-exports.mjs.md b/test/snapshots/markdownlint-test-exports.mjs.md index f03e1f08..a29305c2 100644 --- a/test/snapshots/markdownlint-test-exports.mjs.md +++ b/test/snapshots/markdownlint-test-exports.mjs.md @@ -37,6 +37,7 @@ Generated by [AVA](https://avajs.dev). 'endOfLineHtmlEntityRe', 'escapeForRegExp', 'expandTildePath', + 'formatLintResults', 'frontMatterHasTitle', 'frontMatterRe', 'getHtmlAttributeRe', diff --git a/test/snapshots/markdownlint-test-exports.mjs.snap b/test/snapshots/markdownlint-test-exports.mjs.snap index bf686f39cc606c40a1d48aa09feaf53c82875be7..124c0ee3cd9caf7f3feeea3ac0e1b1d1f25ca113 100644 GIT binary patch literal 1564 zcmV+%2IKibRzVHpznGp{vB6<^C5Cl>2B%VCD;07vq2nwD=1P?0FoD?y4(>>GmdWj1< zr=kD9s`|dF@B3H1*Lo`=?5^s^Unj$jAW3Ucs0;wQ`oa#&PVw)6>&%+HhPr z9mje1Dm?E@!l}+UtszsZL3lw5E={8Tl2K=~>K>-oWjA4FCdSNsjv0`MNa=)n=P#*9 zg+s0>p?=IzTXU2)>w@+$Arh`NIp%J>2G`{()Jz&K1WyJmW(MrM#(+qXF`QHaf0co+ zy9TUCur&%2cCIpJXYy>aK@F#RX9YN10UoLV7b?JWmCduM9#V>}I}_=DQ&Us%fXTNj zz+s=xulREHo9hhwZw>E&~25_zcJlg=?Yyh7(fbSc?m8;*T z-F%RhLXZ{T1@^nZX&2}f;A%1TCtToZ7kIS*)(W8ez6*Tf0^hj6Z!S>xfL$JN&;w3* zK-bGpP%lid;}H*d%mZHXfR8=kk_R-KfZqfbo4~my@N5%!vk83B1b%7)^%mf_fO}fN zxfbwB3;47JTy6o|+Q32^;BDZ^Ht>2oA3&oJfcH@w_^b_l*9QJ*1JfPA?*PX-z@P&> z)dAk>MeoDeevv;`B&%&4`&yUX@hep4U%?_l`(s$*MKBt{|q4nV~om z4u>Soawejhwb`x+B^~c2=aZ^+yBC}4eM+QSBh|KbLwTN*Sq_&-k3@PBdsEQ;2XJ2s zdjA04mV(U`tOwL%BVn^?rGiUB@JREXO{JYmTSx|{dMCJ~yJ;AnLeFR4C0NGltYLrkCUEIfdqIp=Y3Rp!4>k?30NzIb@9DDXA|u z#HlaHfXjZ$ZB4lyeKJciMzTvMxLiFxWPLiy3`@~%wr~bapE^%UFuOI|ZDI2GPNL1K zJY$$LaWfCe8K2Ff!|BjLHPM({?D=Cr85-vK@MTM%j3Hq{>24U%#!7zjKr7|l zp>m<2& z6`vXVDzNsuvqW`8s42+8){19icq9yu1Z~Dwa^o#ornIhg;XTvm# zabc6PVOpnlL*Yk?dgF(ZHRqN#DlArn^<#G=?7@1;&#nLK0p5iHHAJvAI#4j#AF+P9H`+a}wDC6<+4mnG OhJONp219~14*&oh81Q@m literal 1544 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