@markuplint/ml-core 4.13.3 → 5.0.0-alpha.0

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 (57) hide show
  1. package/ARCHITECTURE.ja.md +92 -35
  2. package/ARCHITECTURE.md +85 -28
  3. package/CHANGELOG.md +50 -0
  4. package/docs/linting-pipeline.ja.md +18 -14
  5. package/docs/linting-pipeline.md +18 -14
  6. package/docs/maintenance.ja.md +1 -1
  7. package/docs/maintenance.md +1 -1
  8. package/docs/ml-dom/attr.ja.md +8 -0
  9. package/docs/ml-dom/attr.md +8 -0
  10. package/docs/ml-dom/block.ja.md +33 -33
  11. package/docs/ml-dom/block.md +33 -33
  12. package/docs/ml-dom/element.ja.md +17 -17
  13. package/docs/ml-dom/element.md +17 -17
  14. package/docs/ml-dom/node.ja.md +4 -5
  15. package/docs/ml-dom/node.md +4 -5
  16. package/docs/ml-dom/others.ja.md +2 -1
  17. package/docs/ml-dom/others.md +5 -4
  18. package/docs/rule-system.ja.md +23 -6
  19. package/docs/rule-system.md +23 -6
  20. package/lib/index.d.ts +4 -3
  21. package/lib/index.js +1 -1
  22. package/lib/ml-core.d.ts +1 -1
  23. package/lib/ml-core.js +142 -82
  24. package/lib/ml-dom/helper/accname.d.ts +8 -0
  25. package/lib/ml-dom/helper/accname.js +71 -55
  26. package/lib/ml-dom/helper/create-node.js +1 -0
  27. package/lib/ml-dom/helper/get-indent.js +17 -29
  28. package/lib/ml-dom/node/attr.js +122 -73
  29. package/lib/ml-dom/node/block.d.ts +3 -3
  30. package/lib/ml-dom/node/block.js +10 -1
  31. package/lib/ml-dom/node/document-type.js +12 -0
  32. package/lib/ml-dom/node/document.d.ts +14 -3
  33. package/lib/ml-dom/node/document.js +75 -34
  34. package/lib/ml-dom/node/dom-token-list.js +17 -30
  35. package/lib/ml-dom/node/element-close-tag.js +1 -0
  36. package/lib/ml-dom/node/element.d.ts +19 -7
  37. package/lib/ml-dom/node/element.js +134 -55
  38. package/lib/ml-dom/node/node-store.js +6 -15
  39. package/lib/ml-dom/node/node.d.ts +3 -1
  40. package/lib/ml-dom/node/node.js +159 -166
  41. package/lib/ml-dom/node/parent-node.js +14 -30
  42. package/lib/ml-dom/node/rule-mapper.js +7 -20
  43. package/lib/ml-dom/node/text.d.ts +7 -0
  44. package/lib/ml-dom/node/text.js +9 -0
  45. package/lib/ml-dom/token/token.js +23 -39
  46. package/lib/ml-rule/create-rule.d.ts +8 -1
  47. package/lib/ml-rule/create-rule.js +0 -9
  48. package/lib/ml-rule/ml-rule-context.js +7 -11
  49. package/lib/ml-rule/ml-rule.d.ts +33 -1
  50. package/lib/ml-rule/ml-rule.js +65 -25
  51. package/lib/ruleset/index.js +6 -0
  52. package/lib/test/index.js +4 -1
  53. package/lib/types.d.ts +2 -1
  54. package/lib/violation-collector.js +15 -28
  55. package/lib/virtual-rule.d.ts +72 -0
  56. package/lib/virtual-rule.js +233 -0
  57. package/package.json +16 -13
@@ -0,0 +1,233 @@
1
+ import { isNamedRuleGroup } from '@markuplint/ml-config';
2
+ const NAMED_NODE_RULE_PATTERN = /^[^/]+\/.+$/;
3
+ /**
4
+ * Expands named nodeRules (or childNodeRules) into virtual MLRule instances.
5
+ *
6
+ * Named entries (those with a `name` property containing `/`) are converted into
7
+ * independent virtual rules that reuse the base rule's verify/fix logic but run
8
+ * under their own alias name. This enables per-check control:
9
+ * - Each virtual rule can be independently enabled/disabled via `rules["alias/name"]: false`
10
+ * - Violations report both the base `ruleId` and the alias `name`
11
+ *
12
+ * **false entry separation**: When a named nodeRule has `rules` entries set to `false`,
13
+ * those entries are separated into unnamed nodeRules. This preserves their semantics
14
+ * as base-rule specificity overrides (disabling the base rule on matching nodes)
15
+ * rather than creating virtual rules that would only disable themselves.
16
+ *
17
+ * **Multi-entry support**: When a named nodeRule has 2+ non-false entries, each
18
+ * entry gets its own virtual rule with a derived name (`name/baseRuleName`).
19
+ * A `groupName` is set so the entire group can be disabled via `rules["groupName"]: false`.
20
+ *
21
+ * Unnamed nodeRules pass through unchanged.
22
+ *
23
+ * @param nodeRules - The nodeRules (or childNodeRules) array from the config
24
+ * @param existingRules - All registered MLRule instances (for base rule lookup)
25
+ * @returns Virtual rules, transformed nodeRules, and any validation errors
26
+ */
27
+ export function expandNamedNodeRules(nodeRules, existingRules) {
28
+ const virtualRules = [];
29
+ const transformedNodeRules = [];
30
+ const errors = [];
31
+ const existingRuleMap = new Map(existingRules.map(r => [r.name, r]));
32
+ const usedAliasNames = new Set();
33
+ for (const nodeRule of nodeRules) {
34
+ if (!nodeRule.name) {
35
+ // Unnamed nodeRule: pass through unchanged
36
+ transformedNodeRules.push(nodeRule);
37
+ continue;
38
+ }
39
+ const namedRuleName = nodeRule.name;
40
+ // Validate name format (must contain /)
41
+ if (!NAMED_NODE_RULE_PATTERN.test(namedRuleName)) {
42
+ errors.push(new Error(`Named nodeRule name must contain "/" (e.g., "scope/rule-name"): "${namedRuleName}"`));
43
+ continue;
44
+ }
45
+ // Separate false entries from non-false entries
46
+ const allEntries = Object.entries(nodeRule.rules ?? {});
47
+ const nonFalseEntries = allEntries.filter(([, config]) => config !== false);
48
+ const falseEntries = allEntries.filter(([, config]) => config === false);
49
+ if (nonFalseEntries.length === 0) {
50
+ errors.push(new Error(`Named nodeRule "${namedRuleName}" must have at least one non-false rule entry`));
51
+ continue;
52
+ }
53
+ // Check for duplicate alias names
54
+ if (usedAliasNames.has(namedRuleName)) {
55
+ errors.push(new Error(`Duplicate named nodeRule: "${namedRuleName}"`));
56
+ continue;
57
+ }
58
+ usedAliasNames.add(namedRuleName);
59
+ // Check for name collision with existing rules
60
+ if (existingRuleMap.has(namedRuleName)) {
61
+ errors.push(new Error(`Named nodeRule "${namedRuleName}" conflicts with an existing rule of the same name`));
62
+ continue;
63
+ }
64
+ // Emit false entries as unnamed nodeRules (base-rule specificity overrides)
65
+ if (falseEntries.length > 0) {
66
+ const falseRules = {};
67
+ for (const [key] of falseEntries) {
68
+ falseRules[key] = false;
69
+ }
70
+ // Safety: stripNamedProperties copies all properties except name/specConformance/rules,
71
+ // then adds replacement rules. T's extra properties (e.g., ChildNodeRule.inheritance) are preserved.
72
+ transformedNodeRules.push(stripNamedProperties(nodeRule, falseRules));
73
+ }
74
+ // Determine whether we need derived names (multi-entry)
75
+ const useGroupName = nonFalseEntries.length > 1;
76
+ const groupName = useGroupName ? namedRuleName : undefined;
77
+ for (const [baseRuleName, ruleConfig] of nonFalseEntries) {
78
+ const baseRule = existingRuleMap.get(baseRuleName);
79
+ if (!baseRule) {
80
+ errors.push(new Error(`Base rule "${baseRuleName}" not found for named nodeRule "${namedRuleName}"`));
81
+ continue;
82
+ }
83
+ // For multi-entry: derived name = "groupName/baseRuleName"
84
+ // For single-entry: use the name directly
85
+ const aliasName = useGroupName ? `${namedRuleName}/${baseRuleName}` : namedRuleName;
86
+ // Check derived name collision
87
+ if (existingRuleMap.has(aliasName)) {
88
+ errors.push(new Error(`Named nodeRule "${aliasName}" conflicts with an existing rule of the same name`));
89
+ continue;
90
+ }
91
+ if (usedAliasNames.has(aliasName) && aliasName !== namedRuleName) {
92
+ errors.push(new Error(`Duplicate named nodeRule: "${aliasName}"`));
93
+ continue;
94
+ }
95
+ if (aliasName !== namedRuleName) {
96
+ usedAliasNames.add(aliasName);
97
+ }
98
+ // Create virtual rule by aliasing the base rule
99
+ const virtualRule = baseRule.createAlias(aliasName, {
100
+ specConformance: nodeRule.specConformance,
101
+ groupName,
102
+ });
103
+ 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
+ // Safety: same as above — T's structural properties are preserved by stripNamedProperties.
107
+ transformedNodeRules.push(stripNamedProperties(nodeRule, { [aliasName]: ruleConfig }));
108
+ }
109
+ }
110
+ return { virtualRules, transformedNodeRules, errors };
111
+ }
112
+ /**
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.
116
+ */
117
+ function stripNamedProperties(nodeRule, replacementRules) {
118
+ const result = {};
119
+ for (const [key, value] of Object.entries(nodeRule)) {
120
+ if (key !== 'name' && key !== 'specConformance' && key !== 'rules') {
121
+ result[key] = value;
122
+ }
123
+ }
124
+ result['rules'] = replacementRules;
125
+ return result;
126
+ }
127
+ /**
128
+ * Expands named rule groups in the `rules` section into virtual MLRule instances.
129
+ *
130
+ * Named rule groups (keys containing `/` whose values are {@link NamedRuleGroup} objects)
131
+ * are converted into independent virtual rules, similar to how `expandNamedNodeRules`
132
+ * handles named nodeRules. This enables per-check control at the global rules level.
133
+ *
134
+ * @param rules - The rules dict from the config
135
+ * @param existingRules - All registered MLRule instances (for base rule lookup)
136
+ * @returns Virtual rules, resolved rules dict, and any validation errors
137
+ */
138
+ export function expandNamedRules(rules, existingRules) {
139
+ const virtualRules = [];
140
+ const resolvedRules = {};
141
+ const errors = [];
142
+ const existingRuleMap = new Map(existingRules.map(r => [r.name, r]));
143
+ const usedAliasNames = new Set();
144
+ for (const [key, value] of Object.entries(rules)) {
145
+ // Non-namespaced keys: pass through as regular rules
146
+ if (!key.includes('/')) {
147
+ resolvedRules[key] = value;
148
+ continue;
149
+ }
150
+ // Wildcard patterns (e.g., "a11y/*"): pass through
151
+ if (key.endsWith('/*')) {
152
+ resolvedRules[key] = value;
153
+ continue;
154
+ }
155
+ // `false`: disable signal — pass through
156
+ if (value === false) {
157
+ resolvedRules[key] = false;
158
+ continue;
159
+ }
160
+ // NamedRuleGroup: expand into virtual rules
161
+ if (isNamedRuleGroup(value)) {
162
+ const groupKey = key;
163
+ const { specConformance, severity: groupSeverity } = value;
164
+ const groupRules = value.rules;
165
+ const entries = Object.entries(groupRules);
166
+ const nonFalseEntries = entries.filter(([, v]) => v !== false);
167
+ if (nonFalseEntries.length === 0) {
168
+ errors.push(new Error(`Named rule group "${groupKey}" must have at least one non-false rule entry`));
169
+ continue;
170
+ }
171
+ // Check for duplicate
172
+ if (usedAliasNames.has(groupKey)) {
173
+ errors.push(new Error(`Duplicate named rule group: "${groupKey}"`));
174
+ continue;
175
+ }
176
+ usedAliasNames.add(groupKey);
177
+ // Check for name collision with existing rules
178
+ if (existingRuleMap.has(groupKey)) {
179
+ errors.push(new Error(`Named rule group "${groupKey}" conflicts with an existing rule of the same name`));
180
+ continue;
181
+ }
182
+ const useGroupName = nonFalseEntries.length > 1;
183
+ const gName = useGroupName ? groupKey : undefined;
184
+ // Determine the defaultSeverity for virtual rules:
185
+ // 1. Group-level severity override (from user config merge)
186
+ // 2. undefined (use base rule's default)
187
+ const effectiveDefaultSeverity = groupSeverity;
188
+ for (const [baseRuleName, ruleConfig] of nonFalseEntries) {
189
+ const baseRule = existingRuleMap.get(baseRuleName);
190
+ if (!baseRule) {
191
+ errors.push(new Error(`Base rule "${baseRuleName}" not found for named rule group "${groupKey}"`));
192
+ continue;
193
+ }
194
+ const aliasName = useGroupName ? `${groupKey}/${baseRuleName}` : groupKey;
195
+ // Check collisions
196
+ if (existingRuleMap.has(aliasName)) {
197
+ errors.push(new Error(`Named rule group "${aliasName}" conflicts with an existing rule of the same name`));
198
+ continue;
199
+ }
200
+ if (usedAliasNames.has(aliasName) && aliasName !== groupKey) {
201
+ errors.push(new Error(`Duplicate named rule: "${aliasName}"`));
202
+ continue;
203
+ }
204
+ if (aliasName !== groupKey) {
205
+ usedAliasNames.add(aliasName);
206
+ }
207
+ // Create virtual rule
208
+ const virtualRule = baseRule.createAlias(aliasName, {
209
+ specConformance,
210
+ groupName: gName,
211
+ defaultSeverity: effectiveDefaultSeverity,
212
+ });
213
+ virtualRules.push(virtualRule);
214
+ // Add the rule config under the alias name
215
+ resolvedRules[aliasName] = ruleConfig;
216
+ }
217
+ }
218
+ else {
219
+ // Not a NamedRuleGroup — treat as regular rule (e.g., user config for a virtual rule)
220
+ resolvedRules[key] = value;
221
+ }
222
+ }
223
+ // Backwards compatibility: if a base rule is set to false in the rules dict,
224
+ // also disable any virtual rules from named rule groups that wrap it.
225
+ // This ensures `"id-duplication": false` still works when the preset wraps
226
+ // the rule in a named rule group like `"a11y/id-duplication"`.
227
+ for (const vRule of virtualRules) {
228
+ if (vRule.baseRuleId && resolvedRules[vRule.baseRuleId] === false) {
229
+ resolvedRules[vRule.name] = false;
230
+ }
231
+ }
232
+ return { virtualRules, resolvedRules: resolvedRules, errors };
233
+ }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@markuplint/ml-core",
3
- "version": "4.13.3",
3
+ "version": "5.0.0-alpha.0",
4
4
  "description": "The core module of markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
7
7
  "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
8
11
  "type": "module",
9
12
  "exports": {
10
13
  ".": {
@@ -27,20 +30,20 @@
27
30
  "./lib/configs.js": "./lib/configs.browser.js"
28
31
  },
29
32
  "dependencies": {
30
- "@markuplint/config-presets": "4.5.14",
31
- "@markuplint/html-parser": "4.6.23",
32
- "@markuplint/html-spec": "4.17.0",
33
- "@markuplint/i18n": "4.7.1",
34
- "@markuplint/ml-ast": "4.4.11",
35
- "@markuplint/ml-config": "4.8.15",
36
- "@markuplint/ml-spec": "4.10.2",
37
- "@markuplint/parser-utils": "4.8.11",
38
- "@markuplint/selector": "4.7.8",
39
- "@markuplint/shared": "4.4.13",
33
+ "@markuplint/config-presets": "5.0.0-alpha.0",
34
+ "@markuplint/html-parser": "5.0.0-alpha.0",
35
+ "@markuplint/html-spec": "5.0.0-alpha.0",
36
+ "@markuplint/i18n": "5.0.0-alpha.0",
37
+ "@markuplint/ml-ast": "5.0.0-alpha.0",
38
+ "@markuplint/ml-config": "5.0.0-alpha.0",
39
+ "@markuplint/ml-spec": "5.0.0-alpha.0",
40
+ "@markuplint/parser-utils": "5.0.0-alpha.0",
41
+ "@markuplint/selector": "5.0.0-alpha.0",
42
+ "@markuplint/shared": "5.0.0-alpha.0",
40
43
  "@types/debug": "4.1.12",
41
44
  "debug": "4.4.3",
42
45
  "is-plain-object": "5.0.0",
43
- "type-fest": "4.41.0"
46
+ "type-fest": "5.4.4"
44
47
  },
45
- "gitHead": "193ee7c1262bbed95424e38efdf1a8e56ff049f4"
48
+ "gitHead": "13dcfc84ec83d87360c720e253383b60767e1b56"
46
49
  }