markuplint 5.5.0 → 5.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -0
- package/lib/cli/command.js +3 -2
- package/lib/suppressions/compute-scope.d.ts +35 -1
- package/lib/suppressions/compute-scope.js +27 -16
- package/lib/suppressions/index.d.ts +1 -1
- package/lib/suppressions/merge-suppressions.d.ts +12 -1
- package/lib/suppressions/merge-suppressions.js +27 -7
- package/lib/suppressions/prune-suppressions.d.ts +3 -2
- package/lib/suppressions/prune-suppressions.js +3 -2
- package/package.json +18 -21
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [5.6.1](https://github.com/markuplint/markuplint/compare/v5.6.0...v5.6.1) (2026-11-13)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package markuplint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [5.6.0](https://github.com/markuplint/markuplint/compare/v5.5.0...v5.6.0) (2026-11-12)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **markuplint:** keep the scope of the suppressions ([4f2409d](https://github.com/markuplint/markuplint/commit/4f2409d1d9c98cefbbe91f7c0f084f5855fafeb5)), closes [#4592](https://github.com/markuplint/markuplint/issues/4592)
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
|
|
7
22
|
|
|
8
23
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -136,6 +136,7 @@ $ markuplint --prune-suppressions "src/**/*.html"
|
|
|
136
136
|
- Only `error`-severity violations are suppressed; `warning` and `info` always pass through.
|
|
137
137
|
- If the number of violations for a file+rule pair **exceeds** the suppressed count, **all** violations for that pair are reported.
|
|
138
138
|
- `--suppress` always exits with code 0 (success).
|
|
139
|
+
- Running `--suppress` again keeps the entries it does not record, scope included, and gives the ones it records the newly computed scope.
|
|
139
140
|
- When a DOM tree is available, each suppression entry includes a **scope selector** (computed via LCA) to narrow suppression to a specific subtree. Entries without `scope` apply to the entire file.
|
|
140
141
|
|
|
141
142
|
```json
|
package/lib/cli/command.js
CHANGED
|
@@ -246,8 +246,9 @@ export async function command(files, options, apiOptions) {
|
|
|
246
246
|
}
|
|
247
247
|
const doc = scopeEngine.document;
|
|
248
248
|
if (doc) {
|
|
249
|
-
// MLNode structurally satisfies PositionedNode (startLine, startCol,
|
|
250
|
-
// id, classList, parentElement, children
|
|
249
|
+
// MLNode structurally satisfies PositionedNode (startLine, startCol, endLine, endCol,
|
|
250
|
+
// localName, id, classList, parentElement, children, and closeTag on an element,
|
|
251
|
+
// are all present). The double cast is
|
|
251
252
|
// needed because TypeScript can't verify structural compatibility between the
|
|
252
253
|
// generic MLNode<T,O> and the plain PositionedNode interface at compile time.
|
|
253
254
|
nodeList = doc.nodeList;
|
|
@@ -50,6 +50,29 @@ export interface ScopedNode {
|
|
|
50
50
|
export interface PositionedNode extends ScopedNode {
|
|
51
51
|
readonly startLine: number;
|
|
52
52
|
readonly startCol: number;
|
|
53
|
+
/**
|
|
54
|
+
* Where the node's source ends, excluded. For an element, the end of its start tag.
|
|
55
|
+
* A violation inside this range belongs to the node (see `findNodeAtPosition`);
|
|
56
|
+
* without it, only a violation at the node's start does.
|
|
57
|
+
*/
|
|
58
|
+
readonly endLine?: number;
|
|
59
|
+
/** The column of `endLine`, excluded. */
|
|
60
|
+
readonly endCol?: number;
|
|
61
|
+
/**
|
|
62
|
+
* The end tag of an element. A document's node list has no end tags, so a
|
|
63
|
+
* violation reported at one (`</P>`) is found through its element.
|
|
64
|
+
*/
|
|
65
|
+
readonly closeTag?: SourceRange | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* @experimental
|
|
69
|
+
* A source range: from its start, included, to its end, excluded.
|
|
70
|
+
*/
|
|
71
|
+
export interface SourceRange {
|
|
72
|
+
readonly startLine: number;
|
|
73
|
+
readonly startCol: number;
|
|
74
|
+
readonly endLine?: number;
|
|
75
|
+
readonly endCol?: number;
|
|
53
76
|
}
|
|
54
77
|
/**
|
|
55
78
|
* @experimental
|
|
@@ -143,6 +166,15 @@ export declare function computeLCA(nodes: readonly ScopedNode[]): ScopedNode | n
|
|
|
143
166
|
export declare function generateUniqueSelector(node: ScopedNode): string | undefined;
|
|
144
167
|
/**
|
|
145
168
|
* @experimental
|
|
169
|
+
* Finds the element a violation at `line`:`col` belongs to.
|
|
170
|
+
*
|
|
171
|
+
* The node is the one `searchNodeAtLocation` of `@markuplint/ml-core` finds,
|
|
172
|
+
* the rule `MLDocument.searchNodeByLocation` and the hover of the language
|
|
173
|
+
* server share: a violation reported at an attribute (`no-duplicate-attr`),
|
|
174
|
+
* inside its value or at an end tag is the element that has it, and of several
|
|
175
|
+
* nodes that hold the position the innermost is taken. A non-element is
|
|
176
|
+
* resolved to its nearest element ancestor. `nodeList` is in document order.
|
|
177
|
+
*
|
|
146
178
|
* The O(n) linear search per violation is deliberate — acceptable for
|
|
147
179
|
* typical document sizes; build a position index only if very large
|
|
148
180
|
* documents make this a measured bottleneck.
|
|
@@ -152,7 +184,9 @@ export declare function findNodeAtPosition(nodeList: readonly PositionedNode[],
|
|
|
152
184
|
* @experimental
|
|
153
185
|
* Computes the scope selector for a set of violations within a document.
|
|
154
186
|
* Only considers error-severity violations. Returns `undefined` when
|
|
155
|
-
* the LCA falls back to file-level (body/html) or when
|
|
187
|
+
* the LCA falls back to file-level (body/html) or when a violation is at no
|
|
188
|
+
* element: the entry counts every violation, and applying a scope counts only
|
|
189
|
+
* those found inside it, so a scope that left one out would not suppress it.
|
|
156
190
|
*
|
|
157
191
|
* @param nodeList - Flat list of all nodes in the document.
|
|
158
192
|
* @param violations - The violations to compute scope for.
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @see https://github.com/markuplint/markuplint/issues/3509
|
|
22
22
|
*/
|
|
23
|
+
import { searchNodeAtLocation } from '@markuplint/ml-core';
|
|
23
24
|
const BODY_HTML_TAGS = new Set(['body', 'html']);
|
|
24
25
|
/**
|
|
25
26
|
* The position that ends a segment of a scope: `:nth-of-type(n)`.
|
|
@@ -372,6 +373,15 @@ function buildSegmentWithNth(node) {
|
|
|
372
373
|
}
|
|
373
374
|
/**
|
|
374
375
|
* @experimental
|
|
376
|
+
* Finds the element a violation at `line`:`col` belongs to.
|
|
377
|
+
*
|
|
378
|
+
* The node is the one `searchNodeAtLocation` of `@markuplint/ml-core` finds,
|
|
379
|
+
* the rule `MLDocument.searchNodeByLocation` and the hover of the language
|
|
380
|
+
* server share: a violation reported at an attribute (`no-duplicate-attr`),
|
|
381
|
+
* inside its value or at an end tag is the element that has it, and of several
|
|
382
|
+
* nodes that hold the position the innermost is taken. A non-element is
|
|
383
|
+
* resolved to its nearest element ancestor. `nodeList` is in document order.
|
|
384
|
+
*
|
|
375
385
|
* The O(n) linear search per violation is deliberate — acceptable for
|
|
376
386
|
* typical document sizes; build a position index only if very large
|
|
377
387
|
* documents make this a measured bottleneck.
|
|
@@ -379,26 +389,26 @@ function buildSegmentWithNth(node) {
|
|
|
379
389
|
export function findNodeAtPosition(
|
|
380
390
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
381
391
|
nodeList, line, col) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return node;
|
|
386
|
-
}
|
|
387
|
-
// Non-element (e.g., attribute) — walk up to parent element
|
|
388
|
-
let parent = node.parentElement;
|
|
389
|
-
while (parent && parent.nodeType !== 1) {
|
|
390
|
-
parent = parent.parentElement;
|
|
391
|
-
}
|
|
392
|
-
return parent;
|
|
393
|
-
}
|
|
392
|
+
const found = searchNodeAtLocation(nodeList, line, col);
|
|
393
|
+
if (!found) {
|
|
394
|
+
return null;
|
|
394
395
|
}
|
|
395
|
-
|
|
396
|
+
if (found.nodeType === 1) {
|
|
397
|
+
return found;
|
|
398
|
+
}
|
|
399
|
+
let parent = found.parentElement;
|
|
400
|
+
while (parent && parent.nodeType !== 1) {
|
|
401
|
+
parent = parent.parentElement;
|
|
402
|
+
}
|
|
403
|
+
return parent;
|
|
396
404
|
}
|
|
397
405
|
/**
|
|
398
406
|
* @experimental
|
|
399
407
|
* Computes the scope selector for a set of violations within a document.
|
|
400
408
|
* Only considers error-severity violations. Returns `undefined` when
|
|
401
|
-
* the LCA falls back to file-level (body/html) or when
|
|
409
|
+
* the LCA falls back to file-level (body/html) or when a violation is at no
|
|
410
|
+
* element: the entry counts every violation, and applying a scope counts only
|
|
411
|
+
* those found inside it, so a scope that left one out would not suppress it.
|
|
402
412
|
*
|
|
403
413
|
* @param nodeList - Flat list of all nodes in the document.
|
|
404
414
|
* @param violations - The violations to compute scope for.
|
|
@@ -413,9 +423,10 @@ nodeList, violations) {
|
|
|
413
423
|
continue;
|
|
414
424
|
}
|
|
415
425
|
const node = findNodeAtPosition(nodeList, v.line, v.col);
|
|
416
|
-
if (node) {
|
|
417
|
-
|
|
426
|
+
if (!node) {
|
|
427
|
+
return undefined;
|
|
418
428
|
}
|
|
429
|
+
nodes.push(node);
|
|
419
430
|
}
|
|
420
431
|
if (nodes.length === 0) {
|
|
421
432
|
return undefined;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
export type { SuppressionsData, SuppressionEntry } from './types.js';
|
|
19
19
|
export type { ApplySuppressionsResult, ApplySuppressionsOptions } from './apply-suppressions.js';
|
|
20
20
|
export type { GenerateSuppressionsOptions } from './generate-suppressions.js';
|
|
21
|
-
export type { ScopedNode, PositionedNode } from './compute-scope.js';
|
|
21
|
+
export type { ScopedNode, PositionedNode, SourceRange } from './compute-scope.js';
|
|
22
22
|
export { computeScopeForViolations, computeLCA, generateUniqueSelector } from './compute-scope.js';
|
|
23
23
|
export { applySuppressions } from './apply-suppressions.js';
|
|
24
24
|
export { generateSuppressions } from './generate-suppressions.js';
|
|
@@ -2,7 +2,18 @@ import type { SuppressionsData } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* @experimental
|
|
4
4
|
* Merges incoming suppressions into existing suppressions.
|
|
5
|
-
* For overlapping entries, takes the maximum count
|
|
5
|
+
* For overlapping entries with the same scope, takes the maximum count;
|
|
6
|
+
* for overlapping entries whose scopes differ, takes the incoming entry.
|
|
7
|
+
*
|
|
8
|
+
* The scope of an overlapping entry is always the incoming one:
|
|
9
|
+
* `generateSuppressions` computes it as the subtree that contains every current
|
|
10
|
+
* violation of the rule, so an existing scope could leave some of them outside
|
|
11
|
+
* and unsuppressed. An incoming entry without a scope covers the whole file, so
|
|
12
|
+
* it drops the existing scope. A count is the number of violations inside its
|
|
13
|
+
* own scope, so when the scope changes, the existing count is not carried over:
|
|
14
|
+
* it would let new violations inside the new scope pass up to a number that
|
|
15
|
+
* scope never held. An entry only on one side is kept as it is, scope included
|
|
16
|
+
* (a rule `--suppress-rule` did not select, a file not linted).
|
|
6
17
|
*
|
|
7
18
|
* @param existing - The current suppressions data.
|
|
8
19
|
* @param incoming - The new suppressions data to merge.
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @experimental
|
|
3
3
|
* Merges incoming suppressions into existing suppressions.
|
|
4
|
-
* For overlapping entries, takes the maximum count
|
|
4
|
+
* For overlapping entries with the same scope, takes the maximum count;
|
|
5
|
+
* for overlapping entries whose scopes differ, takes the incoming entry.
|
|
6
|
+
*
|
|
7
|
+
* The scope of an overlapping entry is always the incoming one:
|
|
8
|
+
* `generateSuppressions` computes it as the subtree that contains every current
|
|
9
|
+
* violation of the rule, so an existing scope could leave some of them outside
|
|
10
|
+
* and unsuppressed. An incoming entry without a scope covers the whole file, so
|
|
11
|
+
* it drops the existing scope. A count is the number of violations inside its
|
|
12
|
+
* own scope, so when the scope changes, the existing count is not carried over:
|
|
13
|
+
* it would let new violations inside the new scope pass up to a number that
|
|
14
|
+
* scope never held. An entry only on one side is kept as it is, scope included
|
|
15
|
+
* (a rule `--suppress-rule` did not select, a file not linted).
|
|
5
16
|
*
|
|
6
17
|
* @param existing - The current suppressions data.
|
|
7
18
|
* @param incoming - The new suppressions data to merge.
|
|
@@ -9,9 +20,9 @@
|
|
|
9
20
|
*/
|
|
10
21
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
11
22
|
export function mergeSuppressions(existing, incoming) {
|
|
12
|
-
//
|
|
13
|
-
// and never accidentally lowers a suppression threshold.
|
|
14
|
-
// reduced explicitly via --prune-suppressions.
|
|
23
|
+
// Under the same scope, uses max(count) to ensure running --suppress multiple
|
|
24
|
+
// times is idempotent and never accidentally lowers a suppression threshold.
|
|
25
|
+
// Counts of an unchanged scope are only reduced explicitly via --prune-suppressions.
|
|
15
26
|
const merged = {};
|
|
16
27
|
// Collect all file paths
|
|
17
28
|
const allFiles = new Set([...Object.keys(existing), ...Object.keys(incoming)]);
|
|
@@ -21,11 +32,20 @@ export function mergeSuppressions(existing, incoming) {
|
|
|
21
32
|
const allRules = new Set([...Object.keys(existingRules), ...Object.keys(incomingRules)]);
|
|
22
33
|
const mergedRules = {};
|
|
23
34
|
for (const ruleId of allRules) {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
mergedRules[ruleId] =
|
|
35
|
+
const existingEntry = existingRules[ruleId];
|
|
36
|
+
const incomingEntry = incomingRules[ruleId];
|
|
37
|
+
mergedRules[ruleId] = mergeEntry(existingEntry, incomingEntry);
|
|
27
38
|
}
|
|
28
39
|
merged[filePath] = mergedRules;
|
|
29
40
|
}
|
|
30
41
|
return merged;
|
|
31
42
|
}
|
|
43
|
+
function mergeEntry(existing, incoming) {
|
|
44
|
+
// Both are never absent: the rule ID comes from one of the two sides.
|
|
45
|
+
const base = incoming ?? existing;
|
|
46
|
+
const count = existing && incoming && existing.scope === incoming.scope
|
|
47
|
+
? Math.max(existing.count, incoming.count)
|
|
48
|
+
: base.count;
|
|
49
|
+
// Only `count` and `scope` are written: a key the file has besides them is not carried over.
|
|
50
|
+
return base.scope === undefined ? { count } : { count, scope: base.scope };
|
|
51
|
+
}
|
|
@@ -11,8 +11,9 @@ import type { SuppressionsData } from './types.js';
|
|
|
11
11
|
*
|
|
12
12
|
* **Note:** Pruning counts all file-level violations, not scope-filtered ones.
|
|
13
13
|
* This is because `pruneSuppressions` does not receive document node lists
|
|
14
|
-
* (required for scope-based violation filtering).
|
|
15
|
-
*
|
|
14
|
+
* (required for scope-based violation filtering). `--suppress` recomputes the scope
|
|
15
|
+
* from the DOM tree, and takes the new count when the scope changes; under an
|
|
16
|
+
* unchanged scope it keeps the higher count (see `mergeSuppressions`).
|
|
16
17
|
*
|
|
17
18
|
* @param currentViolations - Map of absolute file paths to current violations.
|
|
18
19
|
* @param existing - The current suppressions data.
|
|
@@ -10,8 +10,9 @@ import { toRelativePath } from './suppressions-file.js';
|
|
|
10
10
|
*
|
|
11
11
|
* **Note:** Pruning counts all file-level violations, not scope-filtered ones.
|
|
12
12
|
* This is because `pruneSuppressions` does not receive document node lists
|
|
13
|
-
* (required for scope-based violation filtering).
|
|
14
|
-
*
|
|
13
|
+
* (required for scope-based violation filtering). `--suppress` recomputes the scope
|
|
14
|
+
* from the DOM tree, and takes the new count when the scope changes; under an
|
|
15
|
+
* unchanged scope it keeps the higher count (see `mergeSuppressions`).
|
|
15
16
|
*
|
|
16
17
|
* @param currentViolations - Map of absolute file paths to current violations.
|
|
17
18
|
* @param existing - The current suppressions data.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markuplint",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.1",
|
|
4
4
|
"description": "An HTML linter for all markup developers",
|
|
5
5
|
"author": "Yusuke Hirao",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,9 +24,6 @@
|
|
|
24
24
|
},
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
|
-
"typedoc": {
|
|
28
|
-
"entryPoint": "./src/index.ts"
|
|
29
|
-
},
|
|
30
27
|
"bin": "./bin/markuplint.mjs",
|
|
31
28
|
"scripts": {
|
|
32
29
|
"build": "tsc --project tsconfig.build.json",
|
|
@@ -34,25 +31,25 @@
|
|
|
34
31
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
35
32
|
},
|
|
36
33
|
"devDependencies": {
|
|
37
|
-
"@markuplint/astro-parser": "5.
|
|
38
|
-
"@markuplint/jsx-parser": "5.
|
|
39
|
-
"@markuplint/mdx-parser": "5.
|
|
40
|
-
"@markuplint/pretenders": "5.
|
|
41
|
-
"@markuplint/svelte-parser": "5.
|
|
42
|
-
"@markuplint/vue-parser": "5.
|
|
34
|
+
"@markuplint/astro-parser": "5.6.1",
|
|
35
|
+
"@markuplint/jsx-parser": "5.6.1",
|
|
36
|
+
"@markuplint/mdx-parser": "5.6.1",
|
|
37
|
+
"@markuplint/pretenders": "5.6.1",
|
|
38
|
+
"@markuplint/svelte-parser": "5.6.1",
|
|
39
|
+
"@markuplint/vue-parser": "5.6.1"
|
|
43
40
|
},
|
|
44
41
|
"dependencies": {
|
|
45
42
|
"@markuplint/cli-utils": "5.3.0",
|
|
46
|
-
"@markuplint/file-resolver": "5.
|
|
47
|
-
"@markuplint/html-parser": "5.
|
|
48
|
-
"@markuplint/html-spec": "5.
|
|
49
|
-
"@markuplint/i18n": "5.
|
|
50
|
-
"@markuplint/ml-ast": "5.
|
|
51
|
-
"@markuplint/ml-config": "5.
|
|
52
|
-
"@markuplint/ml-core": "5.
|
|
53
|
-
"@markuplint/ml-spec": "5.
|
|
54
|
-
"@markuplint/rules": "5.
|
|
55
|
-
"@markuplint/shared": "5.
|
|
43
|
+
"@markuplint/file-resolver": "5.6.1",
|
|
44
|
+
"@markuplint/html-parser": "5.6.1",
|
|
45
|
+
"@markuplint/html-spec": "5.6.1",
|
|
46
|
+
"@markuplint/i18n": "5.6.0",
|
|
47
|
+
"@markuplint/ml-ast": "5.6.0",
|
|
48
|
+
"@markuplint/ml-config": "5.6.1",
|
|
49
|
+
"@markuplint/ml-core": "5.6.1",
|
|
50
|
+
"@markuplint/ml-spec": "5.6.1",
|
|
51
|
+
"@markuplint/rules": "5.6.1",
|
|
52
|
+
"@markuplint/shared": "5.6.0",
|
|
56
53
|
"@types/debug": "^4.1.13",
|
|
57
54
|
"chokidar": "^6.0.0",
|
|
58
55
|
"debug": "^4.4.3",
|
|
@@ -90,5 +87,5 @@
|
|
|
90
87
|
"optional": true
|
|
91
88
|
}
|
|
92
89
|
},
|
|
93
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "a9245162df3699c4880469f493aaaf6103343d83"
|
|
94
91
|
}
|