@markuplint/ml-core 5.0.0-rc.2 → 5.0.0-rc.5
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 +37 -0
- package/README.md +0 -5
- package/lib/cursor-offset.js +0 -3
- package/lib/fix-applier.js +3 -9
- package/lib/ml-core.d.ts +18 -2
- package/lib/ml-core.js +223 -61
- package/lib/ml-dom/helper/accname.d.ts +0 -8
- package/lib/ml-dom/helper/accname.js +7 -10
- package/lib/ml-dom/node/attr.js +3 -1
- package/lib/ml-dom/node/block.d.ts +6 -0
- package/lib/ml-dom/node/block.js +6 -0
- package/lib/ml-dom/node/child-node.d.ts +0 -9
- package/lib/ml-dom/node/child-node.js +0 -9
- package/lib/ml-dom/node/document.d.ts +20 -1
- package/lib/ml-dom/node/document.js +24 -13
- package/lib/ml-dom/node/element-close-tag.d.ts +12 -0
- package/lib/ml-dom/node/element-close-tag.js +12 -0
- package/lib/ml-dom/node/element.d.ts +22 -0
- package/lib/ml-dom/node/element.js +37 -11
- package/lib/ml-dom/node/node-store.d.ts +0 -3
- package/lib/ml-dom/node/node-store.js +0 -3
- package/lib/ml-dom/node/node.d.ts +34 -1
- package/lib/ml-dom/node/node.js +34 -16
- package/lib/ml-dom/node/parent-node.js +0 -6
- package/lib/ml-dom/node/rule-mapper.d.ts +8 -0
- package/lib/ml-dom/node/rule-mapper.js +8 -0
- package/lib/ml-rule/ml-rule.d.ts +19 -0
- package/lib/ml-rule/ml-rule.js +38 -7
- package/lib/ml-rule/types.d.ts +110 -1
- package/lib/ml-rule/types.js +28 -1
- package/lib/ruleset/index.d.ts +2 -1
- package/lib/ruleset/index.js +2 -1
- package/lib/test/index.js +1 -1
- package/lib/virtual-rule.d.ts +10 -0
- package/lib/virtual-rule.js +1 -24
- package/package.json +14 -14
- package/ARCHITECTURE.ja.md +0 -676
- package/ARCHITECTURE.md +0 -726
- package/SKILL.md +0 -61
- package/docs/linting-pipeline.ja.md +0 -307
- package/docs/linting-pipeline.md +0 -307
- package/docs/maintenance.ja.md +0 -210
- package/docs/maintenance.md +0 -210
- package/docs/ml-dom/attr.ja.md +0 -103
- package/docs/ml-dom/attr.md +0 -103
- package/docs/ml-dom/block.ja.md +0 -272
- package/docs/ml-dom/block.md +0 -272
- package/docs/ml-dom/document.ja.md +0 -134
- package/docs/ml-dom/document.md +0 -134
- package/docs/ml-dom/element.ja.md +0 -161
- package/docs/ml-dom/element.md +0 -161
- package/docs/ml-dom/helpers.ja.md +0 -203
- package/docs/ml-dom/helpers.md +0 -203
- package/docs/ml-dom/node.ja.md +0 -199
- package/docs/ml-dom/node.md +0 -199
- package/docs/ml-dom/others.ja.md +0 -120
- package/docs/ml-dom/others.md +0 -120
- package/docs/ml-dom/overview.ja.md +0 -102
- package/docs/ml-dom/overview.md +0 -102
- package/docs/ml-dom/pretender.ja.md +0 -269
- package/docs/ml-dom/pretender.md +0 -269
- package/docs/ml-dom/rule-mapping.ja.md +0 -371
- package/docs/ml-dom/rule-mapping.md +0 -371
- package/docs/ml-dom.ja.md +0 -18
- package/docs/ml-dom.md +0 -18
- package/docs/rule-system.ja.md +0 -287
- package/docs/rule-system.md +0 -287
package/lib/ml-rule/ml-rule.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { RuleSeed } from './types.js';
|
|
|
2
2
|
import type { MLDocument } from '../ml-dom/node/document.js';
|
|
3
3
|
import type { Ruleset } from '../ruleset/index.js';
|
|
4
4
|
import type { LocaleSet } from '@markuplint/i18n';
|
|
5
|
+
import type { MLASTParseErrorCode } from '@markuplint/ml-ast';
|
|
5
6
|
import type { GlobalRuleInfo, SpecConformance, PlainData, Rule, RuleConfigValue, RuleInfo, Severity, Violation } from '@markuplint/ml-config';
|
|
6
7
|
/**
|
|
7
8
|
* Represents a single markuplint rule that can verify documents and report violations.
|
|
@@ -28,8 +29,26 @@ export declare class MLRule<T extends RuleConfigValue, O extends PlainData = und
|
|
|
28
29
|
/**
|
|
29
30
|
* The spec conformance classification of this rule, based on RFC 2119 keyword strength.
|
|
30
31
|
* Set on virtual rules derived from named nodeRules in presets.
|
|
32
|
+
*
|
|
33
|
+
* Intentionally restricted to named (preset-authored) entries: built-in
|
|
34
|
+
* rules already bake normative strength into `defaultSeverity`, and
|
|
35
|
+
* user-defined nodeRules express project conventions rather than spec
|
|
36
|
+
* requirements — allowing it on arbitrary config would blur "the HTML spec
|
|
37
|
+
* requires this" with "our team prefers this". The `/`-containing `name`
|
|
38
|
+
* requirement acts as the gatekeeper. It is reporting metadata only and
|
|
39
|
+
* never influences severity; severity changes require an explicit
|
|
40
|
+
* `defaultSeverity` override on the alias.
|
|
31
41
|
*/
|
|
32
42
|
readonly specConformance?: SpecConformance;
|
|
43
|
+
/**
|
|
44
|
+
* parse5 `ERR` codes this rule's detection covers. When the rule is active
|
|
45
|
+
* in the ruleset, ml-core's built-in `parse-error` channel skips events
|
|
46
|
+
* whose code is in this list, so users do not see duplicate violations.
|
|
47
|
+
*
|
|
48
|
+
* Declared per-rule in `meta.mirrorsParseErrorCodes`; defaults to an empty
|
|
49
|
+
* array when the rule does not mirror any parse5 event.
|
|
50
|
+
*/
|
|
51
|
+
readonly mirrorsParseErrorCodes: readonly MLASTParseErrorCode[];
|
|
33
52
|
constructor(o: Readonly<RuleSeed<T, O>> & {
|
|
34
53
|
readonly name: string;
|
|
35
54
|
readonly baseRuleId?: string;
|
package/lib/ml-rule/ml-rule.js
CHANGED
|
@@ -28,8 +28,26 @@ export class MLRule {
|
|
|
28
28
|
/**
|
|
29
29
|
* The spec conformance classification of this rule, based on RFC 2119 keyword strength.
|
|
30
30
|
* Set on virtual rules derived from named nodeRules in presets.
|
|
31
|
+
*
|
|
32
|
+
* Intentionally restricted to named (preset-authored) entries: built-in
|
|
33
|
+
* rules already bake normative strength into `defaultSeverity`, and
|
|
34
|
+
* user-defined nodeRules express project conventions rather than spec
|
|
35
|
+
* requirements — allowing it on arbitrary config would blur "the HTML spec
|
|
36
|
+
* requires this" with "our team prefers this". The `/`-containing `name`
|
|
37
|
+
* requirement acts as the gatekeeper. It is reporting metadata only and
|
|
38
|
+
* never influences severity; severity changes require an explicit
|
|
39
|
+
* `defaultSeverity` override on the alias.
|
|
31
40
|
*/
|
|
32
41
|
specConformance;
|
|
42
|
+
/**
|
|
43
|
+
* parse5 `ERR` codes this rule's detection covers. When the rule is active
|
|
44
|
+
* in the ruleset, ml-core's built-in `parse-error` channel skips events
|
|
45
|
+
* whose code is in this list, so users do not see duplicate violations.
|
|
46
|
+
*
|
|
47
|
+
* Declared per-rule in `meta.mirrorsParseErrorCodes`; defaults to an empty
|
|
48
|
+
* array when the rule does not mirror any parse5 event.
|
|
49
|
+
*/
|
|
50
|
+
mirrorsParseErrorCodes;
|
|
33
51
|
#v;
|
|
34
52
|
constructor(o) {
|
|
35
53
|
this.name = o.name;
|
|
@@ -42,6 +60,7 @@ export class MLRule {
|
|
|
42
60
|
// See: https://github.com/markuplint/markuplint/issues/808
|
|
43
61
|
this.defaultValue = (o.defaultValue === undefined ? true : o.defaultValue);
|
|
44
62
|
this.defaultOptions = o.defaultOptions;
|
|
63
|
+
this.mirrorsParseErrorCodes = o.meta?.mirrorsParseErrorCodes ?? [];
|
|
45
64
|
this.#v = o.verify;
|
|
46
65
|
}
|
|
47
66
|
/**
|
|
@@ -62,6 +81,10 @@ export class MLRule {
|
|
|
62
81
|
defaultSeverity: options?.defaultSeverity ?? this.defaultSeverity,
|
|
63
82
|
defaultValue: this.defaultValue,
|
|
64
83
|
defaultOptions: this.defaultOptions,
|
|
84
|
+
// Inherit `mirrorsParseErrorCodes` so virtual rules (alias-named
|
|
85
|
+
// nodeRules) participate in the parse-error dedupe under their
|
|
86
|
+
// base rule's mirror list.
|
|
87
|
+
meta: { mirrorsParseErrorCodes: this.mirrorsParseErrorCodes },
|
|
65
88
|
verify: this.#v,
|
|
66
89
|
});
|
|
67
90
|
}
|
|
@@ -116,6 +139,7 @@ export class MLRule {
|
|
|
116
139
|
: configSettings.value,
|
|
117
140
|
options: mergeOptions(this.defaultOptions, configSettings.options),
|
|
118
141
|
reason: configSettings.reason,
|
|
142
|
+
...(configSettings.reasonOnly === undefined ? {} : { reasonOnly: configSettings.reasonOnly }),
|
|
119
143
|
};
|
|
120
144
|
}
|
|
121
145
|
return {
|
|
@@ -147,7 +171,6 @@ export class MLRule {
|
|
|
147
171
|
// Only include name and specConformance for virtual rules (named nodeRules)
|
|
148
172
|
const aliasName = this.baseRuleId ? this.name : undefined;
|
|
149
173
|
const violations = context.reports.map(report => {
|
|
150
|
-
// Execute fix callback if fix mode is enabled
|
|
151
174
|
let fixData;
|
|
152
175
|
if (fix && report.fix) {
|
|
153
176
|
const edits = report.fix(sharedFixer);
|
|
@@ -167,29 +190,25 @@ export class MLRule {
|
|
|
167
190
|
}
|
|
168
191
|
return {
|
|
169
192
|
severity: report.scope.rule.severity,
|
|
170
|
-
message
|
|
193
|
+
...resolveMessageAndReason(report.message, report.scope.rule.reason ?? document.rule.reason, report.scope.rule.reasonOnly ?? document.rule.reasonOnly),
|
|
171
194
|
line,
|
|
172
195
|
col,
|
|
173
196
|
raw,
|
|
174
197
|
ruleId,
|
|
175
198
|
...(aliasName != null && { name: aliasName }),
|
|
176
199
|
...(this.specConformance != null && { specConformance: this.specConformance }),
|
|
177
|
-
...((report.scope.rule.reason ?? document.rule.reason)
|
|
178
|
-
? { reason: report.scope.rule.reason ?? document.rule.reason }
|
|
179
|
-
: {}),
|
|
180
200
|
...(fixData != null && { fix: fixData }),
|
|
181
201
|
};
|
|
182
202
|
}
|
|
183
203
|
return {
|
|
184
204
|
severity: document.rule.severity,
|
|
185
|
-
|
|
205
|
+
...resolveMessageAndReason(report.message, document.rule.reason, document.rule.reasonOnly),
|
|
186
206
|
line: report.line,
|
|
187
207
|
col: report.col,
|
|
188
208
|
raw: report.raw,
|
|
189
209
|
ruleId,
|
|
190
210
|
...(aliasName != null && { name: aliasName }),
|
|
191
211
|
...(this.specConformance != null && { specConformance: this.specConformance }),
|
|
192
|
-
...(document.rule.reason ? { reason: document.rule.reason } : {}),
|
|
193
212
|
...(fixData != null && { fix: fixData }),
|
|
194
213
|
};
|
|
195
214
|
});
|
|
@@ -205,6 +224,18 @@ export class MLRule {
|
|
|
205
224
|
function isRuleConfig(data) {
|
|
206
225
|
return isPlainObject(data);
|
|
207
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolves a violation's `message`/`reason` pair from the rule's configured `reason`/`reasonOnly`.
|
|
229
|
+
* When `reasonOnly` is set and a `reason` is configured, `reason` replaces `message` entirely and
|
|
230
|
+
* is omitted from the result (it is now redundant with `message`); otherwise `reason` is appended
|
|
231
|
+
* as a separate field, preserving the original `message`.
|
|
232
|
+
*/
|
|
233
|
+
function resolveMessageAndReason(message, reason, reasonOnly) {
|
|
234
|
+
if (reason && reasonOnly) {
|
|
235
|
+
return { message: reason };
|
|
236
|
+
}
|
|
237
|
+
return { message, ...(reason ? { reason } : {}) };
|
|
238
|
+
}
|
|
208
239
|
function mergeOptions(a, b) {
|
|
209
240
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
210
241
|
// @ts-ignore
|
package/lib/ml-rule/types.d.ts
CHANGED
|
@@ -1,7 +1,77 @@
|
|
|
1
1
|
import type { MLRuleContext } from './ml-rule-context.js';
|
|
2
2
|
import type { Attr, Element } from '../ml-dom/index.js';
|
|
3
3
|
import type { Translator } from '@markuplint/i18n';
|
|
4
|
+
import type { MLASTParseErrorCode } from '@markuplint/ml-ast';
|
|
4
5
|
import type { PlainData, Report, RuleConfigValue, Severity } from '@markuplint/ml-config';
|
|
6
|
+
/**
|
|
7
|
+
* The specification, standard, or guidance document a rule's check derives from.
|
|
8
|
+
* A rule may cite more than one (e.g. a check spanning HTML LS and WAI-ARIA).
|
|
9
|
+
*
|
|
10
|
+
* - `'html'` — WHATWG HTML Living Standard
|
|
11
|
+
* - `'wai-aria'` — W3C WAI-ARIA
|
|
12
|
+
* - `'aria-in-html'` — W3C ARIA in HTML
|
|
13
|
+
* - `'dpub-aria'` — W3C Digital Publishing WAI-ARIA Module
|
|
14
|
+
* - `'graphics-aria'` — W3C Graphics ARIA
|
|
15
|
+
* - `'accname'` — W3C Accessible Name and Description Computation
|
|
16
|
+
* - `'apg'` — W3C ARIA Authoring Practices Guide (non-normative)
|
|
17
|
+
* - `'wcag'` — W3C Web Content Accessibility Guidelines (success criteria or
|
|
18
|
+
* non-normative techniques — see {@link RuleConformanceLevel})
|
|
19
|
+
* - `'bcd'` — MDN `browser-compat-data` (a factual dataset, not a specification)
|
|
20
|
+
* - `'none'` — no spec basis; a tool-defined or user-configurable check
|
|
21
|
+
*/
|
|
22
|
+
export type RuleConformanceSource = 'html' | 'wai-aria' | 'aria-in-html' | 'dpub-aria' | 'graphics-aria' | 'accname' | 'apg' | 'wcag' | 'bcd' | 'none';
|
|
23
|
+
/**
|
|
24
|
+
* How strongly a rule's check is backed by its {@link RuleConformanceSource}s.
|
|
25
|
+
* This is the basis {@link deriveDefaultSeverityFromConformanceLevel} uses to
|
|
26
|
+
* derive the policy default severity — see that function for the mapping.
|
|
27
|
+
*
|
|
28
|
+
* - `'must'` — a normative MUST/MUST NOT (including HTML parse errors, and a
|
|
29
|
+
* WCAG success criterion violation)
|
|
30
|
+
* - `'should'` — a normative SHOULD/SHOULD NOT
|
|
31
|
+
* - `'non-normative'` — a spec Note, an APG practice, or a WCAG technique
|
|
32
|
+
* - `'factual'` — checked against factual data (e.g. `bcd`) rather than a
|
|
33
|
+
* conformance requirement
|
|
34
|
+
* - `'opinion'` — the spec permits the pattern; the rule enforces a tool or
|
|
35
|
+
* team preference
|
|
36
|
+
* - `'configurable'` — a user-defined constraint with no spec basis; the rule
|
|
37
|
+
* is a mechanism the user configures, not a fixed check
|
|
38
|
+
*/
|
|
39
|
+
export type RuleConformanceLevel = 'must' | 'should' | 'non-normative' | 'factual' | 'opinion' | 'configurable';
|
|
40
|
+
/**
|
|
41
|
+
* A citation for a rule's {@link RuleConformance}: either a bare spec URL, or
|
|
42
|
+
* a WCAG success criterion reference (`sc`/`level`) alongside its URL.
|
|
43
|
+
*/
|
|
44
|
+
export type RuleConformanceCite = string | {
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly sc?: string;
|
|
47
|
+
readonly level?: 'A' | 'AA' | 'AAA';
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* A rule's specification-conformance classification: what it's checking
|
|
51
|
+
* against, how strongly that source requires it, and where to read the
|
|
52
|
+
* requirement. Distinct from `@markuplint/ml-config`'s `SpecConformance`
|
|
53
|
+
* (`'normative' | 'non-normative'`), which is a coarse, optional tag a
|
|
54
|
+
* *user* attaches to their own named nodeRule groups for reporting and never
|
|
55
|
+
* affects severity. This classification is authored by the *rule*, is (in
|
|
56
|
+
* the medium term) mandatory on every built-in rule, and is the basis for
|
|
57
|
+
* the rule's default severity and its eligibility for spec-conformance
|
|
58
|
+
* presets such as `markuplint:html-standard`.
|
|
59
|
+
*/
|
|
60
|
+
export type RuleConformance = {
|
|
61
|
+
readonly sources: readonly RuleConformanceSource[];
|
|
62
|
+
readonly level: RuleConformanceLevel;
|
|
63
|
+
readonly cites: readonly RuleConformanceCite[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Derives the policy default `Severity` for a {@link RuleConformanceLevel},
|
|
67
|
+
* or `undefined` for `'configurable'` (no policy applies — see
|
|
68
|
+
* {@link CONFORMANCE_LEVEL_DEFAULT_SEVERITY}).
|
|
69
|
+
*
|
|
70
|
+
* Used by `@markuplint/rules`'s registry test to flag rules whose
|
|
71
|
+
* `defaultSeverity` disagrees with their declared `specConformance.level`
|
|
72
|
+
* without an accompanying `severityRationale`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function deriveDefaultSeverityFromConformanceLevel(level: RuleConformanceLevel): Severity | undefined;
|
|
5
75
|
/**
|
|
6
76
|
* The definition of a markuplint rule, including verification logic
|
|
7
77
|
* and default configuration values.
|
|
@@ -11,7 +81,46 @@ import type { PlainData, Report, RuleConfigValue, Severity } from '@markuplint/m
|
|
|
11
81
|
*/
|
|
12
82
|
export type RuleSeed<T extends RuleConfigValue = boolean, O extends PlainData = undefined> = {
|
|
13
83
|
readonly meta?: {
|
|
14
|
-
readonly category?: '
|
|
84
|
+
readonly category?: 'syntax' | 'structure' | 'attributes' | 'references' | 'forms' | 'a11y' | 'style' | 'maintainability' | 'compat';
|
|
85
|
+
/**
|
|
86
|
+
* This rule's specification-conformance classification. See
|
|
87
|
+
* {@link RuleConformance}. Intended to become mandatory on every
|
|
88
|
+
* built-in rule; until the v5 rule-system redesign finishes rolling
|
|
89
|
+
* it out across `@markuplint/rules`, it remains optional so partially
|
|
90
|
+
* migrated rules do not fail the build.
|
|
91
|
+
*/
|
|
92
|
+
readonly specConformance?: RuleConformance;
|
|
93
|
+
/**
|
|
94
|
+
* Explains why this rule's `defaultSeverity` deliberately departs
|
|
95
|
+
* from the policy value {@link deriveDefaultSeverityFromConformanceLevel}
|
|
96
|
+
* derives from `meta.specConformance.level`. Required whenever the
|
|
97
|
+
* two disagree; the `@markuplint/rules` registry test enforces this.
|
|
98
|
+
*/
|
|
99
|
+
readonly severityRationale?: string;
|
|
100
|
+
/**
|
|
101
|
+
* parse5 `ERR` codes whose detection this rule covers. When the rule is
|
|
102
|
+
* active in the ruleset, `@markuplint/ml-core`'s built-in `parse-error`
|
|
103
|
+
* channel skips events whose `code` appears in this list — so the user
|
|
104
|
+
* does not get duplicate violations for the same underlying parse5
|
|
105
|
+
* event (one from this rule, one from the parse-error channel).
|
|
106
|
+
*
|
|
107
|
+
* Declare a code here only if the rule's detection scope is at least
|
|
108
|
+
* as broad as the parse5 event (i.e. every situation where parse5
|
|
109
|
+
* fires the code, the rule reports it too). For rules whose detection
|
|
110
|
+
* is wider than parse5 (e.g. `no-duplicate-attr` also covers JSX where
|
|
111
|
+
* parse5 never runs) this still holds because the rule fires first.
|
|
112
|
+
*
|
|
113
|
+
* The dedupe is global (the rule is checked at the ruleset level, not
|
|
114
|
+
* per node), so partial-scope rules like `no-duplicate-attr` (HTML
|
|
115
|
+
* elements only via parse5; JSX / SVG via the rule itself) are safe:
|
|
116
|
+
* parse5 only fires on HTML anyway. Note that `nodeRules` entries that
|
|
117
|
+
* locally disable a mirroring rule do not change this decision.
|
|
118
|
+
*
|
|
119
|
+
* A mirroring rule typically produces its own violation by reading
|
|
120
|
+
* `document.parseErrors`; see the `character-reference` rule in
|
|
121
|
+
* `@markuplint/rules` for the canonical hook example.
|
|
122
|
+
*/
|
|
123
|
+
readonly mirrorsParseErrorCodes?: readonly MLASTParseErrorCode[];
|
|
15
124
|
};
|
|
16
125
|
readonly defaultSeverity?: Severity;
|
|
17
126
|
readonly defaultValue?: T;
|
package/lib/ml-rule/types.js
CHANGED
|
@@ -1 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The policy-derived default `Severity` for each {@link RuleConformanceLevel}.
|
|
3
|
+
* `'configurable'` has no policy value — a rule at that level either omits
|
|
4
|
+
* `defaultSeverity` (falling back to the engine's own `'error'` default,
|
|
5
|
+
* matching `no-restricted-*`-style rules that are inert until configured) or
|
|
6
|
+
* sets one deliberately to fit how it's typically used.
|
|
7
|
+
*
|
|
8
|
+
* @see deriveDefaultSeverityFromConformanceLevel
|
|
9
|
+
*/
|
|
10
|
+
const CONFORMANCE_LEVEL_DEFAULT_SEVERITY = {
|
|
11
|
+
must: 'error',
|
|
12
|
+
should: 'warning',
|
|
13
|
+
'non-normative': 'warning',
|
|
14
|
+
factual: 'warning',
|
|
15
|
+
opinion: 'warning',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Derives the policy default `Severity` for a {@link RuleConformanceLevel},
|
|
19
|
+
* or `undefined` for `'configurable'` (no policy applies — see
|
|
20
|
+
* {@link CONFORMANCE_LEVEL_DEFAULT_SEVERITY}).
|
|
21
|
+
*
|
|
22
|
+
* Used by `@markuplint/rules`'s registry test to flag rules whose
|
|
23
|
+
* `defaultSeverity` disagrees with their declared `specConformance.level`
|
|
24
|
+
* without an accompanying `severityRationale`.
|
|
25
|
+
*/
|
|
26
|
+
export function deriveDefaultSeverityFromConformanceLevel(level) {
|
|
27
|
+
return CONFORMANCE_LEVEL_DEFAULT_SEVERITY[level];
|
|
28
|
+
}
|
package/lib/ruleset/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type { ChildNodeRule, Config, NodeRule, Rules } from '@markuplint/ml-conf
|
|
|
6
6
|
export declare class Ruleset {
|
|
7
7
|
/**
|
|
8
8
|
* Maps base rule names to their virtual rule names created by NamedRuleGroups.
|
|
9
|
-
* For example, if `a11y/
|
|
9
|
+
* For example, if `a11y/landmark-roles` wraps `no-nested-top-level-landmark`, this maps
|
|
10
|
+
* `"no-nested-top-level-landmark"` → `["a11y/landmark-roles"]`.
|
|
10
11
|
* Used by nodeRules/childNodeRules to propagate settings to virtual rules.
|
|
11
12
|
*/
|
|
12
13
|
readonly baseRuleToVirtualNames: ReadonlyMap<string, readonly string[]>;
|
package/lib/ruleset/index.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
export class Ruleset {
|
|
6
6
|
/**
|
|
7
7
|
* Maps base rule names to their virtual rule names created by NamedRuleGroups.
|
|
8
|
-
* For example, if `a11y/
|
|
8
|
+
* For example, if `a11y/landmark-roles` wraps `no-nested-top-level-landmark`, this maps
|
|
9
|
+
* `"no-nested-top-level-landmark"` → `["a11y/landmark-roles"]`.
|
|
9
10
|
* Used by nodeRules/childNodeRules to propagate settings to virtual rules.
|
|
10
11
|
*/
|
|
11
12
|
baseRuleToVirtualNames;
|
package/lib/test/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export function createTestDocument(sourceCode, options) {
|
|
|
19
19
|
: options.parser.parse(sourceCode, options.config?.parserOptions)
|
|
20
20
|
: parser.parse(sourceCode, options?.config?.parserOptions);
|
|
21
21
|
const ruleset = convertRuleset(options?.config);
|
|
22
|
-
const document = new MLDocument(ast, ruleset, [options?.specs ??
|
|
22
|
+
const document = new MLDocument(ast, ruleset, [options?.specs ?? spec, {}], { ariaVersion: ARIA_RECOMMENDED_VERSION }, options?.pretenders ? { pretenders: options.pretenders } : undefined);
|
|
23
23
|
return document;
|
|
24
24
|
}
|
|
25
25
|
/**
|
package/lib/virtual-rule.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminology policy: "virtual rule" is an internal implementation term for
|
|
3
|
+
* contributors only. User-facing documentation (website, migration guides,
|
|
4
|
+
* README) must say "named rule" instead — from a config user's perspective
|
|
5
|
+
* there are only two concepts, a base rule (e.g. `required-attr`) and a named
|
|
6
|
+
* rule (e.g. `a11y/html-lang`); the `MLRule` aliasing mechanics implemented
|
|
7
|
+
* here are intentionally not exposed.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
1
11
|
import type { AnyMLRule } from './ml-rule/index.js';
|
|
2
12
|
import type { Rules, SpecConformance } from '@markuplint/ml-config';
|
|
3
13
|
/**
|
package/lib/virtual-rule.js
CHANGED
|
@@ -32,17 +32,14 @@ export function expandNamedNodeRules(nodeRules, existingRules) {
|
|
|
32
32
|
const usedAliasNames = new Set();
|
|
33
33
|
for (const nodeRule of nodeRules) {
|
|
34
34
|
if (!nodeRule.name) {
|
|
35
|
-
// Unnamed nodeRule: pass through unchanged
|
|
36
35
|
transformedNodeRules.push(nodeRule);
|
|
37
36
|
continue;
|
|
38
37
|
}
|
|
39
38
|
const namedRuleName = nodeRule.name;
|
|
40
|
-
// Validate name format (must contain /)
|
|
41
39
|
if (!NAMED_NODE_RULE_PATTERN.test(namedRuleName)) {
|
|
42
40
|
errors.push(new Error(`Named nodeRule name must contain "/" (e.g., "scope/rule-name"): "${namedRuleName}"`));
|
|
43
41
|
continue;
|
|
44
42
|
}
|
|
45
|
-
// Separate false entries from non-false entries
|
|
46
43
|
const allEntries = Object.entries(nodeRule.rules ?? {});
|
|
47
44
|
const nonFalseEntries = allEntries.filter(([, config]) => config !== false);
|
|
48
45
|
const falseEntries = allEntries.filter(([, config]) => config === false);
|
|
@@ -50,13 +47,11 @@ export function expandNamedNodeRules(nodeRules, existingRules) {
|
|
|
50
47
|
errors.push(new Error(`Named nodeRule "${namedRuleName}" must have at least one non-false rule entry`));
|
|
51
48
|
continue;
|
|
52
49
|
}
|
|
53
|
-
// Check for duplicate alias names
|
|
54
50
|
if (usedAliasNames.has(namedRuleName)) {
|
|
55
51
|
errors.push(new Error(`Duplicate named nodeRule: "${namedRuleName}"`));
|
|
56
52
|
continue;
|
|
57
53
|
}
|
|
58
54
|
usedAliasNames.add(namedRuleName);
|
|
59
|
-
// Check for name collision with existing rules
|
|
60
55
|
if (existingRuleMap.has(namedRuleName)) {
|
|
61
56
|
errors.push(new Error(`Named nodeRule "${namedRuleName}" conflicts with an existing rule of the same name`));
|
|
62
57
|
continue;
|
|
@@ -71,7 +66,6 @@ export function expandNamedNodeRules(nodeRules, existingRules) {
|
|
|
71
66
|
// then adds replacement rules. T's extra properties (e.g., ChildNodeRule.inheritance) are preserved.
|
|
72
67
|
transformedNodeRules.push(stripNamedProperties(nodeRule, falseRules));
|
|
73
68
|
}
|
|
74
|
-
// Determine whether we need derived names (multi-entry)
|
|
75
69
|
const useGroupName = nonFalseEntries.length > 1;
|
|
76
70
|
const groupName = useGroupName ? namedRuleName : undefined;
|
|
77
71
|
for (const [baseRuleName, ruleConfig] of nonFalseEntries) {
|
|
@@ -80,10 +74,7 @@ export function expandNamedNodeRules(nodeRules, existingRules) {
|
|
|
80
74
|
errors.push(new Error(`Base rule "${baseRuleName}" not found for named nodeRule "${namedRuleName}"`));
|
|
81
75
|
continue;
|
|
82
76
|
}
|
|
83
|
-
// For multi-entry: derived name = "groupName/baseRuleName"
|
|
84
|
-
// For single-entry: use the name directly
|
|
85
77
|
const aliasName = useGroupName ? `${namedRuleName}/${baseRuleName}` : namedRuleName;
|
|
86
|
-
// Check derived name collision
|
|
87
78
|
if (existingRuleMap.has(aliasName)) {
|
|
88
79
|
errors.push(new Error(`Named nodeRule "${aliasName}" conflicts with an existing rule of the same name`));
|
|
89
80
|
continue;
|
|
@@ -95,14 +86,11 @@ export function expandNamedNodeRules(nodeRules, existingRules) {
|
|
|
95
86
|
if (aliasName !== namedRuleName) {
|
|
96
87
|
usedAliasNames.add(aliasName);
|
|
97
88
|
}
|
|
98
|
-
// Create virtual rule by aliasing the base rule
|
|
99
89
|
const virtualRule = baseRule.createAlias(aliasName, {
|
|
100
90
|
specConformance: nodeRule.specConformance,
|
|
101
91
|
groupName,
|
|
102
92
|
});
|
|
103
93
|
virtualRules.push(virtualRule);
|
|
104
|
-
// Transform the nodeRule: change the rules key from base rule name to alias name,
|
|
105
|
-
// and strip the name/specConformance properties (consumed by the virtual rule)
|
|
106
94
|
// Safety: same as above — T's structural properties are preserved by stripNamedProperties.
|
|
107
95
|
transformedNodeRules.push(stripNamedProperties(nodeRule, { [aliasName]: ruleConfig }));
|
|
108
96
|
}
|
|
@@ -110,9 +98,7 @@ export function expandNamedNodeRules(nodeRules, existingRules) {
|
|
|
110
98
|
return { virtualRules, transformedNodeRules, errors };
|
|
111
99
|
}
|
|
112
100
|
/**
|
|
113
|
-
*
|
|
114
|
-
* then adds the given replacement rules. Used to transform named nodeRules into
|
|
115
|
-
* their expanded form.
|
|
101
|
+
* Used to transform named nodeRules into their expanded form.
|
|
116
102
|
*/
|
|
117
103
|
function stripNamedProperties(nodeRule, replacementRules) {
|
|
118
104
|
const result = {};
|
|
@@ -142,22 +128,18 @@ export function expandNamedRules(rules, existingRules) {
|
|
|
142
128
|
const existingRuleMap = new Map(existingRules.map(r => [r.name, r]));
|
|
143
129
|
const usedAliasNames = new Set();
|
|
144
130
|
for (const [key, value] of Object.entries(rules)) {
|
|
145
|
-
// Non-namespaced keys: pass through as regular rules
|
|
146
131
|
if (!key.includes('/')) {
|
|
147
132
|
resolvedRules[key] = value;
|
|
148
133
|
continue;
|
|
149
134
|
}
|
|
150
|
-
// Wildcard patterns (e.g., "a11y/*"): pass through
|
|
151
135
|
if (key.endsWith('/*')) {
|
|
152
136
|
resolvedRules[key] = value;
|
|
153
137
|
continue;
|
|
154
138
|
}
|
|
155
|
-
// `false`: disable signal — pass through
|
|
156
139
|
if (value === false) {
|
|
157
140
|
resolvedRules[key] = false;
|
|
158
141
|
continue;
|
|
159
142
|
}
|
|
160
|
-
// NamedRuleGroup: expand into virtual rules
|
|
161
143
|
if (isNamedRuleGroup(value)) {
|
|
162
144
|
const groupKey = key;
|
|
163
145
|
const { specConformance, severity: groupSeverity } = value;
|
|
@@ -168,13 +150,11 @@ export function expandNamedRules(rules, existingRules) {
|
|
|
168
150
|
errors.push(new Error(`Named rule group "${groupKey}" must have at least one non-false rule entry`));
|
|
169
151
|
continue;
|
|
170
152
|
}
|
|
171
|
-
// Check for duplicate
|
|
172
153
|
if (usedAliasNames.has(groupKey)) {
|
|
173
154
|
errors.push(new Error(`Duplicate named rule group: "${groupKey}"`));
|
|
174
155
|
continue;
|
|
175
156
|
}
|
|
176
157
|
usedAliasNames.add(groupKey);
|
|
177
|
-
// Check for name collision with existing rules
|
|
178
158
|
if (existingRuleMap.has(groupKey)) {
|
|
179
159
|
errors.push(new Error(`Named rule group "${groupKey}" conflicts with an existing rule of the same name`));
|
|
180
160
|
continue;
|
|
@@ -192,7 +172,6 @@ export function expandNamedRules(rules, existingRules) {
|
|
|
192
172
|
continue;
|
|
193
173
|
}
|
|
194
174
|
const aliasName = useGroupName ? `${groupKey}/${baseRuleName}` : groupKey;
|
|
195
|
-
// Check collisions
|
|
196
175
|
if (existingRuleMap.has(aliasName)) {
|
|
197
176
|
errors.push(new Error(`Named rule group "${aliasName}" conflicts with an existing rule of the same name`));
|
|
198
177
|
continue;
|
|
@@ -204,14 +183,12 @@ export function expandNamedRules(rules, existingRules) {
|
|
|
204
183
|
if (aliasName !== groupKey) {
|
|
205
184
|
usedAliasNames.add(aliasName);
|
|
206
185
|
}
|
|
207
|
-
// Create virtual rule
|
|
208
186
|
const virtualRule = baseRule.createAlias(aliasName, {
|
|
209
187
|
specConformance,
|
|
210
188
|
groupName: gName,
|
|
211
189
|
defaultSeverity: effectiveDefaultSeverity,
|
|
212
190
|
});
|
|
213
191
|
virtualRules.push(virtualRule);
|
|
214
|
-
// Add the rule config under the alias name
|
|
215
192
|
resolvedRules[aliasName] = ruleConfig;
|
|
216
193
|
}
|
|
217
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/ml-core",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.5",
|
|
4
4
|
"description": "The core module of markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=24"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
@@ -34,20 +34,20 @@
|
|
|
34
34
|
"./lib/configs.js": "./lib/configs.browser.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@markuplint/config-presets": "5.0.0-rc.
|
|
38
|
-
"@markuplint/html-parser": "5.0.0-rc.
|
|
39
|
-
"@markuplint/html-spec": "5.0.0-rc.
|
|
40
|
-
"@markuplint/i18n": "5.0.0-rc.
|
|
41
|
-
"@markuplint/ml-ast": "5.0.0-rc.
|
|
42
|
-
"@markuplint/ml-config": "5.0.0-rc.
|
|
43
|
-
"@markuplint/ml-spec": "5.0.0-rc.
|
|
44
|
-
"@markuplint/parser-utils": "5.0.0-rc.
|
|
45
|
-
"@markuplint/selector": "5.0.0-rc.
|
|
46
|
-
"@markuplint/shared": "5.0.0-rc.
|
|
37
|
+
"@markuplint/config-presets": "5.0.0-rc.5",
|
|
38
|
+
"@markuplint/html-parser": "5.0.0-rc.5",
|
|
39
|
+
"@markuplint/html-spec": "5.0.0-rc.5",
|
|
40
|
+
"@markuplint/i18n": "5.0.0-rc.5",
|
|
41
|
+
"@markuplint/ml-ast": "5.0.0-rc.5",
|
|
42
|
+
"@markuplint/ml-config": "5.0.0-rc.5",
|
|
43
|
+
"@markuplint/ml-spec": "5.0.0-rc.5",
|
|
44
|
+
"@markuplint/parser-utils": "5.0.0-rc.5",
|
|
45
|
+
"@markuplint/selector": "5.0.0-rc.5",
|
|
46
|
+
"@markuplint/shared": "5.0.0-rc.5",
|
|
47
47
|
"@types/debug": "4.1.13",
|
|
48
48
|
"debug": "4.4.3",
|
|
49
49
|
"is-plain-object": "5.0.0",
|
|
50
|
-
"type-fest": "5.
|
|
50
|
+
"type-fest": "5.6.0"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
|
|
53
53
|
}
|