Reimplement MD009/no-trailing-spaces using micromark tokens.

This commit is contained in:
David Anson 2024-08-12 23:24:32 -07:00
parent 37ab4a0faf
commit 4072cf7417
7 changed files with 380 additions and 116 deletions

View file

@ -181,17 +181,6 @@ function isBlankLine(line) {
} }
module.exports.isBlankLine = isBlankLine; module.exports.isBlankLine = isBlankLine;
/**
* Compare function for Array.prototype.sort for ascending order of numbers.
*
* @param {number} a First number.
* @param {number} b Second number.
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
*/
module.exports.numericSortAscending = function numericSortAscending(a, b) {
return a - b;
};
// Returns true iff the sorted array contains the specified element // Returns true iff the sorted array contains the specified element
module.exports.includesSorted = function includesSorted(array, element) { module.exports.includesSorted = function includesSorted(array, element) {
let left = 0; let left = 0;
@ -3747,9 +3736,22 @@ module.exports = {
const { addError, filterTokens, forEachLine, includesSorted, const { addError } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
numericSortAscending } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"); const { filterByTypes } = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs");
const { lineMetadata } = __webpack_require__(/*! ./cache */ "../lib/cache.js");
/**
* Adds a range of numbers to a set.
*
* @param {Set<number>} set Set of numbers.
* @param {number} start Starting number.
* @param {number} end Ending number.
* @returns {void}
*/
function addRangeToSet(set, start, end) {
for (let i = start; i <= end; i++) {
set.add(i);
}
}
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type import("./markdownlint").Rule */ /** @type import("./markdownlint").Rule */
@ -3757,63 +3759,69 @@ module.exports = {
"names": [ "MD009", "no-trailing-spaces" ], "names": [ "MD009", "no-trailing-spaces" ],
"description": "Trailing spaces", "description": "Trailing spaces",
"tags": [ "whitespace" ], "tags": [ "whitespace" ],
"parser": "markdownit", "parser": "micromark",
"function": function MD009(params, onError) { "function": function MD009(params, onError) {
let brSpaces = params.config.br_spaces; let brSpaces = params.config.br_spaces;
brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces); brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces);
const listItemEmptyLines = !!params.config.list_item_empty_lines; const listItemEmptyLines = !!params.config.list_item_empty_lines;
const strict = !!params.config.strict; const strict = !!params.config.strict;
const listItemLineNumbers = []; const { tokens } = params.parsers.micromark;
if (listItemEmptyLines) { const codeBlockLineNumbers = new Set();
filterTokens(params, "list_item_open", (token) => { for (const codeBlock of filterByTypes(tokens, [ "codeFenced" ])) {
for (let i = token.map[0]; i < token.map[1]; i++) { addRangeToSet(codeBlockLineNumbers, codeBlock.startLine + 1, codeBlock.endLine - 1);
listItemLineNumbers.push(i + 1);
}
});
listItemLineNumbers.sort(numericSortAscending);
} }
const paragraphLineNumbers = []; for (const codeBlock of filterByTypes(tokens, [ "codeIndented" ])) {
const codeInlineLineNumbers = []; addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
const listItemLineNumbers = new Set();
if (listItemEmptyLines) {
for (const listBlock of filterByTypes(tokens, [ "listOrdered", "listUnordered" ])) {
addRangeToSet(listItemLineNumbers, listBlock.startLine, listBlock.endLine);
let trailingIndent = true;
for (let i = listBlock.children.length - 1; i >= 0; i--) {
const child = listBlock.children[i];
switch (child.type) {
case "content":
trailingIndent = false;
break;
case "listItemIndent":
if (trailingIndent) {
listItemLineNumbers.delete(child.startLine);
}
break;
case "listItemPrefix":
trailingIndent = true;
break;
default:
break;
}
}
}
}
const paragraphLineNumbers = new Set();
const codeInlineLineNumbers = new Set();
if (strict) { if (strict) {
filterTokens(params, "paragraph_open", (token) => { for (const paragraph of filterByTypes(tokens, [ "paragraph" ])) {
for (let i = token.map[0]; i < token.map[1] - 1; i++) { addRangeToSet(paragraphLineNumbers, paragraph.startLine, paragraph.endLine - 1);
paragraphLineNumbers.push(i + 1); }
} for (const codeText of filterByTypes(tokens, [ "codeText" ])) {
}); addRangeToSet(codeInlineLineNumbers, codeText.startLine, codeText.endLine - 1);
const addLineNumberRange = (start, end) => { }
for (let i = start; i < end; i++) {
codeInlineLineNumbers.push(i);
}
};
filterTokens(params, "inline", (token) => {
let start = 0;
for (const child of token.children) {
if (start > 0) {
addLineNumberRange(start, child.lineNumber);
start = 0;
}
if (child.type === "code_inline") {
start = child.lineNumber;
}
}
if (start > 0) {
addLineNumberRange(start, token.map[1]);
}
});
} }
const expected = (brSpaces < 2) ? 0 : brSpaces; const expected = (brSpaces < 2) ? 0 : brSpaces;
forEachLine(lineMetadata(), (line, lineIndex, inCode) => { for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
const lineNumber = lineIndex + 1; const lineNumber = lineIndex + 1;
const trailingSpaces = line.length - line.trimEnd().length; const trailingSpaces = line.length - line.trimEnd().length;
if ( if (
trailingSpaces && trailingSpaces &&
!inCode && !codeBlockLineNumbers.has(lineNumber) &&
!includesSorted(listItemLineNumbers, lineNumber) && !listItemLineNumbers.has(lineNumber) &&
( (
(expected !== trailingSpaces) || (expected !== trailingSpaces) ||
(strict && (strict &&
(!includesSorted(paragraphLineNumbers, lineNumber) || (!paragraphLineNumbers.has(lineNumber) ||
includesSorted(codeInlineLineNumbers, lineNumber))) codeInlineLineNumbers.has(lineNumber)))
) )
) { ) {
const column = line.length - trailingSpaces + 1; const column = line.length - trailingSpaces + 1;
@ -3827,9 +3835,10 @@ module.exports = {
{ {
"editColumn": column, "editColumn": column,
"deleteCount": trailingSpaces "deleteCount": trailingSpaces
}); }
);
} }
}); }
} }
}; };

View file

@ -169,17 +169,6 @@ function isBlankLine(line) {
} }
module.exports.isBlankLine = isBlankLine; module.exports.isBlankLine = isBlankLine;
/**
* Compare function for Array.prototype.sort for ascending order of numbers.
*
* @param {number} a First number.
* @param {number} b Second number.
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
*/
module.exports.numericSortAscending = function numericSortAscending(a, b) {
return a - b;
};
// Returns true iff the sorted array contains the specified element // Returns true iff the sorted array contains the specified element
module.exports.includesSorted = function includesSorted(array, element) { module.exports.includesSorted = function includesSorted(array, element) {
let left = 0; let left = 0;

View file

@ -2,9 +2,22 @@
"use strict"; "use strict";
const { addError, filterTokens, forEachLine, includesSorted, const { addError } = require("../helpers");
numericSortAscending } = require("../helpers"); const { filterByTypes } = require("../helpers/micromark.cjs");
const { lineMetadata } = require("./cache");
/**
* Adds a range of numbers to a set.
*
* @param {Set<number>} set Set of numbers.
* @param {number} start Starting number.
* @param {number} end Ending number.
* @returns {void}
*/
function addRangeToSet(set, start, end) {
for (let i = start; i <= end; i++) {
set.add(i);
}
}
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type import("./markdownlint").Rule */ /** @type import("./markdownlint").Rule */
@ -12,63 +25,69 @@ module.exports = {
"names": [ "MD009", "no-trailing-spaces" ], "names": [ "MD009", "no-trailing-spaces" ],
"description": "Trailing spaces", "description": "Trailing spaces",
"tags": [ "whitespace" ], "tags": [ "whitespace" ],
"parser": "markdownit", "parser": "micromark",
"function": function MD009(params, onError) { "function": function MD009(params, onError) {
let brSpaces = params.config.br_spaces; let brSpaces = params.config.br_spaces;
brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces); brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces);
const listItemEmptyLines = !!params.config.list_item_empty_lines; const listItemEmptyLines = !!params.config.list_item_empty_lines;
const strict = !!params.config.strict; const strict = !!params.config.strict;
const listItemLineNumbers = []; const { tokens } = params.parsers.micromark;
if (listItemEmptyLines) { const codeBlockLineNumbers = new Set();
filterTokens(params, "list_item_open", (token) => { for (const codeBlock of filterByTypes(tokens, [ "codeFenced" ])) {
for (let i = token.map[0]; i < token.map[1]; i++) { addRangeToSet(codeBlockLineNumbers, codeBlock.startLine + 1, codeBlock.endLine - 1);
listItemLineNumbers.push(i + 1);
}
});
listItemLineNumbers.sort(numericSortAscending);
} }
const paragraphLineNumbers = []; for (const codeBlock of filterByTypes(tokens, [ "codeIndented" ])) {
const codeInlineLineNumbers = []; addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
const listItemLineNumbers = new Set();
if (listItemEmptyLines) {
for (const listBlock of filterByTypes(tokens, [ "listOrdered", "listUnordered" ])) {
addRangeToSet(listItemLineNumbers, listBlock.startLine, listBlock.endLine);
let trailingIndent = true;
for (let i = listBlock.children.length - 1; i >= 0; i--) {
const child = listBlock.children[i];
switch (child.type) {
case "content":
trailingIndent = false;
break;
case "listItemIndent":
if (trailingIndent) {
listItemLineNumbers.delete(child.startLine);
}
break;
case "listItemPrefix":
trailingIndent = true;
break;
default:
break;
}
}
}
}
const paragraphLineNumbers = new Set();
const codeInlineLineNumbers = new Set();
if (strict) { if (strict) {
filterTokens(params, "paragraph_open", (token) => { for (const paragraph of filterByTypes(tokens, [ "paragraph" ])) {
for (let i = token.map[0]; i < token.map[1] - 1; i++) { addRangeToSet(paragraphLineNumbers, paragraph.startLine, paragraph.endLine - 1);
paragraphLineNumbers.push(i + 1); }
} for (const codeText of filterByTypes(tokens, [ "codeText" ])) {
}); addRangeToSet(codeInlineLineNumbers, codeText.startLine, codeText.endLine - 1);
const addLineNumberRange = (start, end) => { }
for (let i = start; i < end; i++) {
codeInlineLineNumbers.push(i);
}
};
filterTokens(params, "inline", (token) => {
let start = 0;
for (const child of token.children) {
if (start > 0) {
addLineNumberRange(start, child.lineNumber);
start = 0;
}
if (child.type === "code_inline") {
start = child.lineNumber;
}
}
if (start > 0) {
addLineNumberRange(start, token.map[1]);
}
});
} }
const expected = (brSpaces < 2) ? 0 : brSpaces; const expected = (brSpaces < 2) ? 0 : brSpaces;
forEachLine(lineMetadata(), (line, lineIndex, inCode) => { for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
const lineNumber = lineIndex + 1; const lineNumber = lineIndex + 1;
const trailingSpaces = line.length - line.trimEnd().length; const trailingSpaces = line.length - line.trimEnd().length;
if ( if (
trailingSpaces && trailingSpaces &&
!inCode && !codeBlockLineNumbers.has(lineNumber) &&
!includesSorted(listItemLineNumbers, lineNumber) && !listItemLineNumbers.has(lineNumber) &&
( (
(expected !== trailingSpaces) || (expected !== trailingSpaces) ||
(strict && (strict &&
(!includesSorted(paragraphLineNumbers, lineNumber) || (!paragraphLineNumbers.has(lineNumber) ||
includesSorted(codeInlineLineNumbers, lineNumber))) codeInlineLineNumbers.has(lineNumber)))
) )
) { ) {
const column = line.length - trailingSpaces + 1; const column = line.length - trailingSpaces + 1;
@ -82,8 +101,9 @@ module.exports = {
{ {
"editColumn": column, "editColumn": column,
"deleteCount": trailingSpaces "deleteCount": trailingSpaces
}); }
);
} }
}); }
} }
}; };

View file

@ -57272,6 +57272,25 @@ Generated by [AVA](https://avajs.dev).
'no-trailing-spaces', 'no-trailing-spaces',
], ],
}, },
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 6',
errorRange: [
1,
6,
],
fixInfo: {
deleteCount: 6,
editColumn: 1,
},
lineNumber: 35,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{ {
errorContext: null, errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 6', errorDetail: 'Expected: 0 or 2; Actual: 6',
@ -57310,6 +57329,82 @@ Generated by [AVA](https://avajs.dev).
'no-trailing-spaces', 'no-trailing-spaces',
], ],
}, },
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 57,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 58,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 60,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 61,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
], ],
fixed: `# Heading␊ fixed: `# Heading␊
@ -57345,7 +57440,7 @@ Generated by [AVA](https://avajs.dev).
1. text␊ 1. text␊
text␊ text␊
1. text␊ 1. text␊
1. text␊ 1. text␊
@ -57361,10 +57456,24 @@ Generated by [AVA](https://avajs.dev).
- text␊ - text␊
text␊ text␊
{MD009:35}␊
{MD009:37}␊ {MD009:37}␊
{MD009:50}␊ {MD009:50}␊
1. text␊
text␊
1. text␊
{MD009:57}␊
{MD009:58}␊
{MD009:60}␊
{MD009:61}␊
<!-- markdownlint-configure-file {␊ <!-- markdownlint-configure-file {␊
"no-multiple-blanks": false,␊
"no-trailing-spaces": {␊ "no-trailing-spaces": {␊
"list_item_empty_lines": true,␊ "list_item_empty_lines": true,␊
"strict": true␊ "strict": true␊
@ -57417,6 +57526,25 @@ Generated by [AVA](https://avajs.dev).
'no-trailing-spaces', 'no-trailing-spaces',
], ],
}, },
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 6',
errorRange: [
1,
6,
],
fixInfo: {
deleteCount: 6,
editColumn: 1,
},
lineNumber: 35,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{ {
errorContext: null, errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 6', errorDetail: 'Expected: 0 or 2; Actual: 6',
@ -57455,6 +57583,82 @@ Generated by [AVA](https://avajs.dev).
'no-trailing-spaces', 'no-trailing-spaces',
], ],
}, },
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 57,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 58,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 60,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
{
errorContext: null,
errorDetail: 'Expected: 0 or 2; Actual: 3',
errorRange: [
1,
3,
],
fixInfo: {
deleteCount: 3,
editColumn: 1,
},
lineNumber: 61,
ruleDescription: 'Trailing spaces',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md009.md',
ruleNames: [
'MD009',
'no-trailing-spaces',
],
},
], ],
fixed: `# Heading␊ fixed: `# Heading␊
@ -57490,7 +57694,7 @@ Generated by [AVA](https://avajs.dev).
1. text␊ 1. text␊
text␊ text␊
1. text␊ 1. text␊
1. text␊ 1. text␊
@ -57506,10 +57710,24 @@ Generated by [AVA](https://avajs.dev).
- text␊ - text␊
text␊ text␊
{MD009:35}␊
{MD009:37}␊ {MD009:37}␊
{MD009:50}␊ {MD009:50}␊
1. text␊
text␊
1. text␊
{MD009:57}␊
{MD009:58}␊
{MD009:60}␊
{MD009:61}␊
<!-- markdownlint-configure-file {␊ <!-- markdownlint-configure-file {␊
"no-multiple-blanks": false,␊
"no-trailing-spaces": {␊ "no-trailing-spaces": {␊
"list_item_empty_lines": true␊ "list_item_empty_lines": true␊
}␊ }␊

View file

@ -48,10 +48,24 @@
- text - text
text text
{MD009:35}
{MD009:37} {MD009:37}
{MD009:50} {MD009:50}
1. text
text
1. text
{MD009:57}
{MD009:58}
{MD009:60}
{MD009:61}
<!-- markdownlint-configure-file { <!-- markdownlint-configure-file {
"no-multiple-blanks": false,
"no-trailing-spaces": { "no-trailing-spaces": {
"list_item_empty_lines": true, "list_item_empty_lines": true,
"strict": true "strict": true

View file

@ -48,10 +48,24 @@
- text - text
text text
{MD009:35}
{MD009:37} {MD009:37}
{MD009:50} {MD009:50}
1. text
text
1. text
{MD009:57}
{MD009:58}
{MD009:60}
{MD009:61}
<!-- markdownlint-configure-file { <!-- markdownlint-configure-file {
"no-multiple-blanks": false,
"no-trailing-spaces": { "no-trailing-spaces": {
"list_item_empty_lines": true "list_item_empty_lines": true
} }