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;
/**
* 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
module.exports.includesSorted = function includesSorted(array, element) {
let left = 0;
@ -3747,9 +3736,22 @@ module.exports = {
const { addError, filterTokens, forEachLine, includesSorted,
numericSortAscending } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
const { lineMetadata } = __webpack_require__(/*! ./cache */ "../lib/cache.js");
const { addError } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
const { filterByTypes } = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs");
/**
* 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
/** @type import("./markdownlint").Rule */
@ -3757,63 +3759,69 @@ module.exports = {
"names": [ "MD009", "no-trailing-spaces" ],
"description": "Trailing spaces",
"tags": [ "whitespace" ],
"parser": "markdownit",
"parser": "micromark",
"function": function MD009(params, onError) {
let brSpaces = params.config.br_spaces;
brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces);
const listItemEmptyLines = !!params.config.list_item_empty_lines;
const strict = !!params.config.strict;
const listItemLineNumbers = [];
if (listItemEmptyLines) {
filterTokens(params, "list_item_open", (token) => {
for (let i = token.map[0]; i < token.map[1]; i++) {
listItemLineNumbers.push(i + 1);
}
});
listItemLineNumbers.sort(numericSortAscending);
const { tokens } = params.parsers.micromark;
const codeBlockLineNumbers = new Set();
for (const codeBlock of filterByTypes(tokens, [ "codeFenced" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine + 1, codeBlock.endLine - 1);
}
const paragraphLineNumbers = [];
const codeInlineLineNumbers = [];
for (const codeBlock of filterByTypes(tokens, [ "codeIndented" ])) {
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) {
filterTokens(params, "paragraph_open", (token) => {
for (let i = token.map[0]; i < token.map[1] - 1; i++) {
paragraphLineNumbers.push(i + 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]);
}
});
for (const paragraph of filterByTypes(tokens, [ "paragraph" ])) {
addRangeToSet(paragraphLineNumbers, paragraph.startLine, paragraph.endLine - 1);
}
for (const codeText of filterByTypes(tokens, [ "codeText" ])) {
addRangeToSet(codeInlineLineNumbers, codeText.startLine, codeText.endLine - 1);
}
}
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 trailingSpaces = line.length - line.trimEnd().length;
if (
trailingSpaces &&
!inCode &&
!includesSorted(listItemLineNumbers, lineNumber) &&
!codeBlockLineNumbers.has(lineNumber) &&
!listItemLineNumbers.has(lineNumber) &&
(
(expected !== trailingSpaces) ||
(strict &&
(!includesSorted(paragraphLineNumbers, lineNumber) ||
includesSorted(codeInlineLineNumbers, lineNumber)))
(!paragraphLineNumbers.has(lineNumber) ||
codeInlineLineNumbers.has(lineNumber)))
)
) {
const column = line.length - trailingSpaces + 1;
@ -3827,9 +3835,10 @@ module.exports = {
{
"editColumn": column,
"deleteCount": trailingSpaces
});
}
);
}
});
}
}
};

View file

@ -169,17 +169,6 @@ function isBlankLine(line) {
}
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
module.exports.includesSorted = function includesSorted(array, element) {
let left = 0;

View file

@ -2,9 +2,22 @@
"use strict";
const { addError, filterTokens, forEachLine, includesSorted,
numericSortAscending } = require("../helpers");
const { lineMetadata } = require("./cache");
const { addError } = require("../helpers");
const { filterByTypes } = require("../helpers/micromark.cjs");
/**
* 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
/** @type import("./markdownlint").Rule */
@ -12,63 +25,69 @@ module.exports = {
"names": [ "MD009", "no-trailing-spaces" ],
"description": "Trailing spaces",
"tags": [ "whitespace" ],
"parser": "markdownit",
"parser": "micromark",
"function": function MD009(params, onError) {
let brSpaces = params.config.br_spaces;
brSpaces = Number((brSpaces === undefined) ? 2 : brSpaces);
const listItemEmptyLines = !!params.config.list_item_empty_lines;
const strict = !!params.config.strict;
const listItemLineNumbers = [];
if (listItemEmptyLines) {
filterTokens(params, "list_item_open", (token) => {
for (let i = token.map[0]; i < token.map[1]; i++) {
listItemLineNumbers.push(i + 1);
}
});
listItemLineNumbers.sort(numericSortAscending);
const { tokens } = params.parsers.micromark;
const codeBlockLineNumbers = new Set();
for (const codeBlock of filterByTypes(tokens, [ "codeFenced" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine + 1, codeBlock.endLine - 1);
}
const paragraphLineNumbers = [];
const codeInlineLineNumbers = [];
for (const codeBlock of filterByTypes(tokens, [ "codeIndented" ])) {
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) {
filterTokens(params, "paragraph_open", (token) => {
for (let i = token.map[0]; i < token.map[1] - 1; i++) {
paragraphLineNumbers.push(i + 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]);
}
});
for (const paragraph of filterByTypes(tokens, [ "paragraph" ])) {
addRangeToSet(paragraphLineNumbers, paragraph.startLine, paragraph.endLine - 1);
}
for (const codeText of filterByTypes(tokens, [ "codeText" ])) {
addRangeToSet(codeInlineLineNumbers, codeText.startLine, codeText.endLine - 1);
}
}
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 trailingSpaces = line.length - line.trimEnd().length;
if (
trailingSpaces &&
!inCode &&
!includesSorted(listItemLineNumbers, lineNumber) &&
!codeBlockLineNumbers.has(lineNumber) &&
!listItemLineNumbers.has(lineNumber) &&
(
(expected !== trailingSpaces) ||
(strict &&
(!includesSorted(paragraphLineNumbers, lineNumber) ||
includesSorted(codeInlineLineNumbers, lineNumber)))
(!paragraphLineNumbers.has(lineNumber) ||
codeInlineLineNumbers.has(lineNumber)))
)
) {
const column = line.length - trailingSpaces + 1;
@ -82,8 +101,9 @@ module.exports = {
{
"editColumn": column,
"deleteCount": trailingSpaces
});
}
);
}
});
}
}
};

View file

@ -57272,6 +57272,25 @@ Generated by [AVA](https://avajs.dev).
'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,
errorDetail: 'Expected: 0 or 2; Actual: 6',
@ -57310,6 +57329,82 @@ Generated by [AVA](https://avajs.dev).
'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␊
@ -57345,7 +57440,7 @@ Generated by [AVA](https://avajs.dev).
1. text␊
text␊
1. text␊
1. text␊
@ -57361,10 +57456,24 @@ Generated by [AVA](https://avajs.dev).
- text␊
text␊
{MD009:35}␊
{MD009:37}␊
{MD009:50}␊
1. text␊
text␊
1. text␊
{MD009:57}␊
{MD009:58}␊
{MD009:60}␊
{MD009:61}␊
<!-- markdownlint-configure-file {␊
"no-multiple-blanks": false,␊
"no-trailing-spaces": {␊
"list_item_empty_lines": true,␊
"strict": true␊
@ -57417,6 +57526,25 @@ Generated by [AVA](https://avajs.dev).
'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,
errorDetail: 'Expected: 0 or 2; Actual: 6',
@ -57455,6 +57583,82 @@ Generated by [AVA](https://avajs.dev).
'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␊
@ -57490,7 +57694,7 @@ Generated by [AVA](https://avajs.dev).
1. text␊
text␊
1. text␊
1. text␊
@ -57506,10 +57710,24 @@ Generated by [AVA](https://avajs.dev).
- text␊
text␊
{MD009:35}␊
{MD009:37}␊
{MD009:50}␊
1. text␊
text␊
1. text␊
{MD009:57}␊
{MD009:58}␊
{MD009:60}␊
{MD009:61}␊
<!-- markdownlint-configure-file {␊
"no-multiple-blanks": false,␊
"no-trailing-spaces": {␊
"list_item_empty_lines": true␊
}␊

View file

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

View file

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