@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +0 -5
  3. package/lib/cursor-offset.js +0 -3
  4. package/lib/fix-applier.js +3 -9
  5. package/lib/ml-core.d.ts +18 -2
  6. package/lib/ml-core.js +223 -61
  7. package/lib/ml-dom/helper/accname.d.ts +0 -8
  8. package/lib/ml-dom/helper/accname.js +7 -10
  9. package/lib/ml-dom/node/attr.js +3 -1
  10. package/lib/ml-dom/node/block.d.ts +6 -0
  11. package/lib/ml-dom/node/block.js +6 -0
  12. package/lib/ml-dom/node/child-node.d.ts +0 -9
  13. package/lib/ml-dom/node/child-node.js +0 -9
  14. package/lib/ml-dom/node/document.d.ts +20 -1
  15. package/lib/ml-dom/node/document.js +24 -13
  16. package/lib/ml-dom/node/element-close-tag.d.ts +12 -0
  17. package/lib/ml-dom/node/element-close-tag.js +12 -0
  18. package/lib/ml-dom/node/element.d.ts +22 -0
  19. package/lib/ml-dom/node/element.js +37 -11
  20. package/lib/ml-dom/node/node-store.d.ts +0 -3
  21. package/lib/ml-dom/node/node-store.js +0 -3
  22. package/lib/ml-dom/node/node.d.ts +34 -1
  23. package/lib/ml-dom/node/node.js +34 -16
  24. package/lib/ml-dom/node/parent-node.js +0 -6
  25. package/lib/ml-dom/node/rule-mapper.d.ts +8 -0
  26. package/lib/ml-dom/node/rule-mapper.js +8 -0
  27. package/lib/ml-rule/ml-rule.d.ts +19 -0
  28. package/lib/ml-rule/ml-rule.js +38 -7
  29. package/lib/ml-rule/types.d.ts +110 -1
  30. package/lib/ml-rule/types.js +28 -1
  31. package/lib/ruleset/index.d.ts +2 -1
  32. package/lib/ruleset/index.js +2 -1
  33. package/lib/test/index.js +1 -1
  34. package/lib/virtual-rule.d.ts +10 -0
  35. package/lib/virtual-rule.js +1 -24
  36. package/package.json +14 -14
  37. package/ARCHITECTURE.ja.md +0 -676
  38. package/ARCHITECTURE.md +0 -726
  39. package/SKILL.md +0 -61
  40. package/docs/linting-pipeline.ja.md +0 -307
  41. package/docs/linting-pipeline.md +0 -307
  42. package/docs/maintenance.ja.md +0 -210
  43. package/docs/maintenance.md +0 -210
  44. package/docs/ml-dom/attr.ja.md +0 -103
  45. package/docs/ml-dom/attr.md +0 -103
  46. package/docs/ml-dom/block.ja.md +0 -272
  47. package/docs/ml-dom/block.md +0 -272
  48. package/docs/ml-dom/document.ja.md +0 -134
  49. package/docs/ml-dom/document.md +0 -134
  50. package/docs/ml-dom/element.ja.md +0 -161
  51. package/docs/ml-dom/element.md +0 -161
  52. package/docs/ml-dom/helpers.ja.md +0 -203
  53. package/docs/ml-dom/helpers.md +0 -203
  54. package/docs/ml-dom/node.ja.md +0 -199
  55. package/docs/ml-dom/node.md +0 -199
  56. package/docs/ml-dom/others.ja.md +0 -120
  57. package/docs/ml-dom/others.md +0 -120
  58. package/docs/ml-dom/overview.ja.md +0 -102
  59. package/docs/ml-dom/overview.md +0 -102
  60. package/docs/ml-dom/pretender.ja.md +0 -269
  61. package/docs/ml-dom/pretender.md +0 -269
  62. package/docs/ml-dom/rule-mapping.ja.md +0 -371
  63. package/docs/ml-dom/rule-mapping.md +0 -371
  64. package/docs/ml-dom.ja.md +0 -18
  65. package/docs/ml-dom.md +0 -18
  66. package/docs/rule-system.ja.md +0 -287
  67. package/docs/rule-system.md +0 -287
@@ -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;
@@ -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: report.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
- message: report.message,
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
@@ -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?: 'validation' | 'style' | 'naming-convention' | 'a11y' | 'maintainability';
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;
@@ -1 +1,28 @@
1
- export {};
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
+ }
@@ -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/wai-aria` wraps `wai-aria`, this maps `"wai-aria"` → `["a11y/wai-aria"]`.
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[]>;
@@ -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/wai-aria` wraps `wai-aria`, this maps `"wai-aria"` → `["a11y/wai-aria"]`.
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 ?? {}, {}], { ariaVersion: ARIA_RECOMMENDED_VERSION }, options?.pretenders ? { pretenders: options.pretenders } : undefined);
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
  /**
@@ -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
  /**
@@ -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
- * Creates a copy of the nodeRule with `name`, `specConformance`, and `rules` removed,
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.2",
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": ">=22"
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.2",
38
- "@markuplint/html-parser": "5.0.0-rc.2",
39
- "@markuplint/html-spec": "5.0.0-rc.2",
40
- "@markuplint/i18n": "5.0.0-rc.2",
41
- "@markuplint/ml-ast": "5.0.0-rc.2",
42
- "@markuplint/ml-config": "5.0.0-rc.2",
43
- "@markuplint/ml-spec": "5.0.0-rc.2",
44
- "@markuplint/parser-utils": "5.0.0-rc.2",
45
- "@markuplint/selector": "5.0.0-rc.2",
46
- "@markuplint/shared": "5.0.0-rc.2",
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.5.0"
50
+ "type-fest": "5.6.0"
51
51
  },
52
- "gitHead": "e43763858d9234c417053becc73dbd088c1e7ea6"
52
+ "gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
53
53
  }