@markuplint/ml-core 5.0.0-rc.1 → 5.0.0-rc.4

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 CHANGED
@@ -3,6 +3,32 @@
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.0.0-rc.4](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.3...v5.0.0-rc.4) (2026-04-19)
7
+
8
+ **Note:** Version bump only for package @markuplint/ml-core
9
+
10
+ # [5.0.0-rc.3](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.2...v5.0.0-rc.3) (2026-04-19)
11
+
12
+ **Note:** Version bump only for package @markuplint/ml-core
13
+
14
+ # [5.0.0-rc.2](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.1...v5.0.0-rc.2) (2026-04-15)
15
+
16
+ ### Bug Fixes
17
+
18
+ - **ml-core:** propagate base rule disable to virtual rules in nodeRules/childNodeRules ([06fa269](https://github.com/markuplint/markuplint/commit/06fa2695d43671adc1ebcadf1304aca44fd10919)), closes [#3578](https://github.com/markuplint/markuplint/issues/3578)
19
+
20
+ - build!: remove ESLint and replace with oxlint ([1e0a337](https://github.com/markuplint/markuplint/commit/1e0a337707f76b903b16beeeb8c4d4fc0d8fc9e4))
21
+ - feat(ml-core)!: remove deprecated getIndent function ([7ab1a2d](https://github.com/markuplint/markuplint/commit/7ab1a2dae8f36002f4e5f3108a400bb9095db319))
22
+
23
+ ### Features
24
+
25
+ - **ml-core:** propagate option overrides to virtual rules in nodeRules/childNodeRules ([9cb9521](https://github.com/markuplint/markuplint/commit/9cb9521fc1303c132f547ffd1e8c82b3b9099bef))
26
+
27
+ ### BREAKING CHANGES
28
+
29
+ - ESLint is no longer used. Use oxlint instead.
30
+ - getIndent() has been removed from the public API.
31
+
6
32
  # [5.0.0-rc.1](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.0...v5.0.0-rc.1) (2026-03-27)
7
33
 
8
34
  ### Bug Fixes
package/lib/index.d.ts CHANGED
@@ -5,7 +5,6 @@ export { enableDebug } from './debug.js';
5
5
  export { computeCursorOffset } from './cursor-offset.js';
6
6
  export { applyFixes } from './fix-applier.js';
7
7
  export type { FixResult } from './fix-applier.js';
8
- export { getIndent } from './ml-dom/helper/get-indent.js';
9
8
  export * from './convert-ruleset.js';
10
9
  export * from './ml-core.js';
11
10
  export * from './ml-dom/index.js';
package/lib/index.js CHANGED
@@ -3,7 +3,6 @@ export { Ruleset } from './ruleset/index.js';
3
3
  export { enableDebug } from './debug.js';
4
4
  export { computeCursorOffset } from './cursor-offset.js';
5
5
  export { applyFixes } from './fix-applier.js';
6
- export { getIndent } from './ml-dom/helper/get-indent.js';
7
6
  export * from './convert-ruleset.js';
8
7
  export * from './ml-core.js';
9
8
  export * from './ml-dom/index.js';
package/lib/ml-core.js CHANGED
@@ -67,6 +67,8 @@ export class MLCore {
67
67
  rules: resolvedRules,
68
68
  nodeRules: nodeRuleResult.transformedNodeRules,
69
69
  childNodeRules: childNodeRuleResult.transformedNodeRules,
70
+ baseRuleToVirtualNames: buildBaseRuleToVirtualNames(namedRulesResult.virtualRules),
71
+ mappingErrors: [],
70
72
  };
71
73
  this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
72
74
  this.#configErrors.push(...namedRulesResult.errors, ...nodeRuleResult.errors, ...childNodeRuleResult.errors);
@@ -100,7 +102,7 @@ export class MLCore {
100
102
  this.#locale = locale ?? this.#locale;
101
103
  this.#schemas = schemas ?? this.#schemas;
102
104
  this.#configErrors = [...(configErrors ?? [])];
103
- const baseRules = rules?.slice() ?? this.#rules.filter(r => !r.baseRuleId);
105
+ const baseRules = rules ? [...rules] : this.#rules.filter(r => !r.baseRuleId);
104
106
  // Use pre-expansion originals as fallback when ruleset is not provided
105
107
  const incomingNodeRules = ruleset?.nodeRules ?? this.#originalNodeRules;
106
108
  const incomingChildNodeRules = ruleset?.childNodeRules ?? this.#originalChildNodeRules;
@@ -123,6 +125,8 @@ export class MLCore {
123
125
  rules: resolvedRules,
124
126
  nodeRules: nodeRuleResult.transformedNodeRules,
125
127
  childNodeRules: childNodeRuleResult.transformedNodeRules,
128
+ baseRuleToVirtualNames: buildBaseRuleToVirtualNames(namedRulesResult.virtualRules),
129
+ mappingErrors: [],
126
130
  };
127
131
  this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
128
132
  this.#configErrors.push(...namedRulesResult.errors, ...nodeRuleResult.errors, ...childNodeRuleResult.errors);
@@ -243,6 +247,9 @@ export class MLCore {
243
247
  tagNameCaseSensitive: this.#parser.tagNameCaseSensitive,
244
248
  pretenders: this.#pretenders,
245
249
  });
250
+ // Collect errors from rule mapping (e.g., invalid wildcard usage)
251
+ this.#configErrors.push(...this.#ruleset.mappingErrors);
252
+ this.#ruleset.mappingErrors.length = 0;
246
253
  }
247
254
  catch (error) {
248
255
  if (error instanceof ParserError) {
@@ -426,6 +433,29 @@ function extractDisabledNamespaces(rules) {
426
433
  .filter(([key, value]) => key.endsWith('/*') && value === false)
427
434
  .map(([key]) => key.slice(0, -1)); // "a11y/*" → "a11y/"
428
435
  }
436
+ /**
437
+ * Builds a mapping from base rule names to virtual rule names.
438
+ * Used by nodeRules/childNodeRules to propagate settings (especially `false`)
439
+ * to virtual rules created by NamedRuleGroups.
440
+ */
441
+ function buildBaseRuleToVirtualNames(
442
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
443
+ virtualRules) {
444
+ const map = new Map();
445
+ for (const vRule of virtualRules) {
446
+ if (!vRule.baseRuleId) {
447
+ continue;
448
+ }
449
+ const list = map.get(vRule.baseRuleId);
450
+ if (list) {
451
+ list.push(vRule.name);
452
+ }
453
+ else {
454
+ map.set(vRule.baseRuleId, [vRule.name]);
455
+ }
456
+ }
457
+ return map;
458
+ }
429
459
  /**
430
460
  * Collects all `FixData` from violations that have a fix callback result.
431
461
  *
@@ -2399,6 +2399,24 @@ export class MLDocument extends MLParentNode {
2399
2399
  if (rule == null) {
2400
2400
  continue;
2401
2401
  }
2402
+ // Namespace wildcard (e.g., "a11y/*": false)
2403
+ if (ruleName.endsWith('/*')) {
2404
+ if (rule !== false) {
2405
+ ruleset.mappingErrors.push(new Error(`Namespace wildcard "${ruleName}" only accepts false; use a specific rule name for options`));
2406
+ continue;
2407
+ }
2408
+ const nsPrefix = ruleName.slice(0, -1); // "a11y/*" → "a11y/"
2409
+ for (const key of Object.keys(ruleset.rules)) {
2410
+ if (key.startsWith(nsPrefix)) {
2411
+ ruleMapper.set(node, key, {
2412
+ from: 'nodeRules',
2413
+ specificity: matches.specificity,
2414
+ rule: false,
2415
+ });
2416
+ }
2417
+ }
2418
+ continue;
2419
+ }
2402
2420
  const convertedRule = exchangeValueOnRule(rule, matches.data ?? {});
2403
2421
  if (convertedRule === undefined) {
2404
2422
  continue;
@@ -2411,6 +2429,19 @@ export class MLDocument extends MLParentNode {
2411
2429
  specificity: matches.specificity,
2412
2430
  rule: mergedRule,
2413
2431
  });
2432
+ // Propagate to virtual rules that wrap this base rule
2433
+ const virtualNames = ruleset.baseRuleToVirtualNames.get(ruleName);
2434
+ if (virtualNames) {
2435
+ for (const vName of virtualNames) {
2436
+ const vGlobalRule = ruleset.rules[vName];
2437
+ const vMergedRule = vGlobalRule == null ? convertedRule : mergeRule(vGlobalRule, convertedRule);
2438
+ ruleMapper.set(node, vName, {
2439
+ from: 'nodeRules',
2440
+ specificity: matches.specificity,
2441
+ rule: vMergedRule,
2442
+ });
2443
+ }
2444
+ }
2414
2445
  }
2415
2446
  }
2416
2447
  // overwrite rule to child node
@@ -2442,6 +2473,26 @@ export class MLDocument extends MLParentNode {
2442
2473
  if (rule == null) {
2443
2474
  continue;
2444
2475
  }
2476
+ // Namespace wildcard (e.g., "a11y/*": false)
2477
+ if (ruleName.endsWith('/*')) {
2478
+ if (rule !== false) {
2479
+ ruleset.mappingErrors.push(new Error(`Namespace wildcard "${ruleName}" only accepts false; use a specific rule name for options`));
2480
+ continue;
2481
+ }
2482
+ const nsPrefix = ruleName.slice(0, -1); // "a11y/*" → "a11y/"
2483
+ for (const key of Object.keys(ruleset.rules)) {
2484
+ if (key.startsWith(nsPrefix)) {
2485
+ for (const descendant of targetDescendants) {
2486
+ ruleMapper.set(descendant, key, {
2487
+ from: 'childNodeRules',
2488
+ specificity: matches.specificity,
2489
+ rule: false,
2490
+ });
2491
+ }
2492
+ }
2493
+ }
2494
+ continue;
2495
+ }
2445
2496
  const convertedRule = exchangeValueOnRule(rule, matches.data ?? {});
2446
2497
  if (convertedRule === undefined) {
2447
2498
  continue;
@@ -2456,6 +2507,21 @@ export class MLDocument extends MLParentNode {
2456
2507
  rule: mergedRule,
2457
2508
  });
2458
2509
  }
2510
+ // Propagate to virtual rules that wrap this base rule
2511
+ const virtualNames = ruleset.baseRuleToVirtualNames.get(ruleName);
2512
+ if (virtualNames) {
2513
+ for (const vName of virtualNames) {
2514
+ const vGlobalRule = ruleset.rules[vName];
2515
+ const vMergedRule = vGlobalRule == null ? convertedRule : mergeRule(vGlobalRule, convertedRule);
2516
+ for (const descendant of targetDescendants) {
2517
+ ruleMapper.set(descendant, vName, {
2518
+ from: 'childNodeRules',
2519
+ specificity: matches.specificity,
2520
+ rule: vMergedRule,
2521
+ });
2522
+ }
2523
+ }
2524
+ }
2459
2525
  }
2460
2526
  }
2461
2527
  }
@@ -4,14 +4,26 @@ import type { ChildNodeRule, Config, NodeRule, Rules } from '@markuplint/ml-conf
4
4
  * rule overrides extracted from a markuplint {@link Config}.
5
5
  */
6
6
  export declare class Ruleset {
7
+ /**
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"]`.
10
+ * Used by nodeRules/childNodeRules to propagate settings to virtual rules.
11
+ */
12
+ readonly baseRuleToVirtualNames: ReadonlyMap<string, readonly string[]>;
7
13
  /** Rule overrides that apply to child nodes matching specific selectors */
8
14
  readonly childNodeRules: readonly ChildNodeRule[];
15
+ /**
16
+ * Errors collected during rule mapping (e.g., invalid wildcard usage).
17
+ * Consumed by MLCore and reported as config-error violations.
18
+ */
19
+ readonly mappingErrors: Error[];
9
20
  /** Rule overrides that apply to nodes matching specific selectors */
10
21
  readonly nodeRules: readonly NodeRule[];
11
22
  /** The global rule definitions */
12
23
  readonly rules: Rules;
13
24
  /**
14
25
  * @param config - The markuplint configuration to extract rules from
26
+ * @param baseRuleToVirtualNames - Mapping from base rule names to virtual rule names
15
27
  */
16
- constructor(config: Config);
28
+ constructor(config: Config, baseRuleToVirtualNames?: ReadonlyMap<string, readonly string[]>);
17
29
  }
@@ -3,18 +3,31 @@
3
3
  * rule overrides extracted from a markuplint {@link Config}.
4
4
  */
5
5
  export class Ruleset {
6
+ /**
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"]`.
9
+ * Used by nodeRules/childNodeRules to propagate settings to virtual rules.
10
+ */
11
+ baseRuleToVirtualNames;
6
12
  /** Rule overrides that apply to child nodes matching specific selectors */
7
13
  childNodeRules;
14
+ /**
15
+ * Errors collected during rule mapping (e.g., invalid wildcard usage).
16
+ * Consumed by MLCore and reported as config-error violations.
17
+ */
18
+ mappingErrors = [];
8
19
  /** Rule overrides that apply to nodes matching specific selectors */
9
20
  nodeRules;
10
21
  /** The global rule definitions */
11
22
  rules;
12
23
  /**
13
24
  * @param config - The markuplint configuration to extract rules from
25
+ * @param baseRuleToVirtualNames - Mapping from base rule names to virtual rule names
14
26
  */
15
- constructor(config) {
27
+ constructor(config, baseRuleToVirtualNames) {
16
28
  this.rules = config.rules ?? {};
17
29
  this.nodeRules = config.nodeRules ?? [];
18
30
  this.childNodeRules = config.childNodeRules ?? [];
31
+ this.baseRuleToVirtualNames = baseRuleToVirtualNames ?? new Map();
19
32
  }
20
33
  }
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@markuplint/ml-core",
3
- "version": "5.0.0-rc.1",
3
+ "version": "5.0.0-rc.4",
4
4
  "description": "The core module of markuplint",
5
- "repository": "git@github.com:markuplint/markuplint.git",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/markuplint/markuplint.git",
8
+ "directory": "packages/@markuplint/ml-core"
9
+ },
6
10
  "author": "Yusuke Hirao <yusukehirao@me.com>",
7
11
  "license": "MIT",
8
12
  "engines": {
@@ -30,20 +34,20 @@
30
34
  "./lib/configs.js": "./lib/configs.browser.js"
31
35
  },
32
36
  "dependencies": {
33
- "@markuplint/config-presets": "5.0.0-rc.1",
34
- "@markuplint/html-parser": "5.0.0-rc.1",
35
- "@markuplint/html-spec": "5.0.0-rc.1",
36
- "@markuplint/i18n": "5.0.0-rc.1",
37
- "@markuplint/ml-ast": "5.0.0-rc.1",
38
- "@markuplint/ml-config": "5.0.0-rc.1",
39
- "@markuplint/ml-spec": "5.0.0-rc.1",
40
- "@markuplint/parser-utils": "5.0.0-rc.1",
41
- "@markuplint/selector": "5.0.0-rc.1",
42
- "@markuplint/shared": "5.0.0-rc.1",
37
+ "@markuplint/config-presets": "5.0.0-rc.4",
38
+ "@markuplint/html-parser": "5.0.0-rc.4",
39
+ "@markuplint/html-spec": "5.0.0-rc.4",
40
+ "@markuplint/i18n": "5.0.0-rc.4",
41
+ "@markuplint/ml-ast": "5.0.0-rc.4",
42
+ "@markuplint/ml-config": "5.0.0-rc.4",
43
+ "@markuplint/ml-spec": "5.0.0-rc.4",
44
+ "@markuplint/parser-utils": "5.0.0-rc.4",
45
+ "@markuplint/selector": "5.0.0-rc.4",
46
+ "@markuplint/shared": "5.0.0-rc.4",
43
47
  "@types/debug": "4.1.13",
44
48
  "debug": "4.4.3",
45
49
  "is-plain-object": "5.0.0",
46
- "type-fest": "5.5.0"
50
+ "type-fest": "5.6.0"
47
51
  },
48
- "gitHead": "0d6b4324d9a7d6b9e1ba57d4a57e45d36975cba9"
52
+ "gitHead": "97a6339bbae23f556de5d307b3ce2ef7cfd9402d"
49
53
  }
@@ -1,20 +0,0 @@
1
- import type { MLNode } from '../node/node.js';
2
- import type { MLText } from '../node/text.js';
3
- /**
4
- * Computes the indentation preceding the given node by analyzing
5
- * the whitespace in adjacent text nodes.
6
- *
7
- * @deprecated
8
- * @param node - The node whose indentation to determine
9
- * @returns An indentation object describing the whitespace, or null if no indentation is found
10
- */
11
- export declare function getIndent(node: MLNode<any, any>): MLDOMIndentation | null;
12
- declare class MLDOMIndentation {
13
- #private;
14
- readonly line: number;
15
- constructor(originTextNode: MLText<any, any>, raw: string, line: number, parentNode: MLNode<any, any>);
16
- get raw(): string;
17
- get type(): 'tab' | 'space' | 'mixed' | 'none';
18
- get width(): number;
19
- }
20
- export {};
@@ -1,90 +0,0 @@
1
- /**
2
- * Computes the indentation preceding the given node by analyzing
3
- * the whitespace in adjacent text nodes.
4
- *
5
- * @deprecated
6
- * @param node - The node whose indentation to determine
7
- * @returns An indentation object describing the whitespace, or null if no indentation is found
8
- */
9
- export function getIndent(
10
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
11
- node) {
12
- const prevToken = node.prevToken;
13
- if (!prevToken) {
14
- return null;
15
- }
16
- if (node.is(node.TEXT_NODE)) {
17
- if (node.isRawTextElementContent()) {
18
- return null;
19
- }
20
- const matched = node.raw.match(/^([\t\v\f\r \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*\n\s*)\S+/);
21
- if (matched) {
22
- const spaces = matched[1];
23
- if (spaces) {
24
- const spaceLines = spaces.split(/\r?\n/);
25
- const line = spaceLines.length + node.startLine - 1;
26
- const lastSpace = spaceLines.pop();
27
- if (lastSpace != null) {
28
- return new MLDOMIndentation(node, lastSpace, line, node);
29
- }
30
- }
31
- }
32
- return null;
33
- }
34
- if (!prevToken.is(node.TEXT_NODE)) {
35
- return null;
36
- }
37
- // One or more newlines and zero or more spaces or tabs.
38
- // Or, If textNode is first token and that is filled spaces, tabs and newlines only.
39
- const matched = isFirstToken(prevToken)
40
- ? prevToken.raw.match(/^(?:[\t ]*\r?\n)*([\t ]*)$/)
41
- : prevToken.raw.match(/\r?\n([\t ]*)$/);
42
- if (matched) {
43
- // Spaces will include empty string.
44
- const spaces = matched[1];
45
- if (spaces != null) {
46
- return new MLDOMIndentation(prevToken, spaces, node.startLine, node);
47
- }
48
- }
49
- return null;
50
- }
51
- class MLDOMIndentation {
52
- #raw;
53
- line;
54
- #node;
55
- #parent;
56
- constructor(
57
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
58
- originTextNode, raw, line,
59
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
60
- parentNode) {
61
- this.line = line;
62
- this.#node = originTextNode;
63
- this.#parent = parentNode;
64
- this.#raw = raw;
65
- }
66
- get raw() {
67
- if (!this.#parent.is(this.#parent.TEXT_NODE) && this.line !== this.#node.endLine) {
68
- return '';
69
- }
70
- return this.#raw;
71
- }
72
- get type() {
73
- if (!this.#parent.is(this.#parent.TEXT_NODE) && this.line !== this.#node.endLine) {
74
- return 'none';
75
- }
76
- const raw = this.#raw;
77
- return raw === '' ? 'none' : /^\t+$/.test(raw) ? 'tab' : /^[^\t]+$/.test(raw) ? 'space' : 'mixed';
78
- }
79
- get width() {
80
- if (!this.#parent.is(this.#parent.TEXT_NODE) && this.line !== this.#node.endLine) {
81
- return 0;
82
- }
83
- return this.#raw.length;
84
- }
85
- }
86
- function isFirstToken(
87
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
88
- node) {
89
- return !node.prevToken;
90
- }