@markuplint/ml-core 5.0.0-alpha.2 → 5.0.0-dev.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.
@@ -439,7 +439,7 @@ sequenceDiagram
439
439
  | `remove(token)` | `startOffset` + `raw` を持つトークン | `range: [start, start+len], text: ""` |
440
440
  | `removeRange(range)` | 明示的な `[start, end)` レンジ | `range: [start, end], text: ""` |
441
441
 
442
- `token` パラメータは `startOffset` `raw` プロパティを持つ任意のオブジェクトを受け付けます。MLDOM トークン(`MLToken`, `MLAttr` 等)は自然にこの要件を満たします。
442
+ `token` パラメータは `FixToken` 型(`@markuplint/ml-config` で定義)を満たす任意のオブジェクト — つまり `{ startOffset: number; raw: string }` — を受け付けます。MLDOM トークン(`MLToken`, `MLAttr` 等)は自然にこの要件を満たします。
443
443
 
444
444
  ### FixApplier アルゴリズム
445
445
 
@@ -468,6 +468,39 @@ flowchart TD
468
468
  - `FixData` 間の重複はスキップ機構で処理される
469
469
  - `FixData` 内のいずれかの edit がスキップされると、その `FixData` 全体がスキップとして分類される
470
470
 
471
+ ### マルチパス Fix ループ
472
+
473
+ `applyFixes()` がレンジの重複により一部の fix をスキップした場合、エンジンはマルチパスループ(`_multiPassFix()`)に入り、再パース・再検証を繰り返して修正可能な違反をすべて解決します:
474
+
475
+ ```mermaid
476
+ flowchart TD
477
+ A["violations から fix を抽出"] --> B["applyFixes(code, fixes)"]
478
+ B --> C{"applied.length === 0?"}
479
+ C -- Yes --> Z["現在のコードを返す"]
480
+ C -- No --> D{"output === currentCode?"}
481
+ D -- Yes --> Z
482
+ D -- No --> E{"サイクル検出?\n(2パス前の出力と一致)"}
483
+ E -- Yes --> Z
484
+ E -- No --> F{"skipped.length === 0?"}
485
+ F -- Yes --> Z["修正済みコードを返す\n(全 fix 適用完了)"]
486
+ F -- No --> G["再パース + 再検証"]
487
+ G --> H{"ParserError?"}
488
+ H -- Yes --> Z["最後の正常なコードに戻す"]
489
+ H -- No --> I{"新たな修正可能な違反?"}
490
+ I -- No --> Z
491
+ I -- Yes --> B
492
+ ```
493
+
494
+ 主な設計ポイント:
495
+
496
+ - **ゼロコストパス**: fix を持つ violation がなければ、マルチパスループは完全にスキップされる
497
+ - **シングルパス高速パス**: `skipped.length === 0` のとき即座にループを抜ける(Phase 1 と同等の動作)
498
+ - **サイクル検出**: 2パス前の出力と比較し、A→B→A の振動パターンを検出
499
+ - **安全上限**: 最大10パス(ESLint の `SourceCodeFixer` と同じ)
500
+ - **状態復元**: `verify()` は `try/finally` で `#sourceCode`、`#ast`、`#document` を保存・復元
501
+
502
+ **重要**: `VerifyResult` の `violations` 配列は初回パスの結果のみを反映し、`fixedCode` は複数パスの結果である場合があります。修正後コードの正確な違反リストが必要な場合は、出力を再検証してください。
503
+
471
504
  ### 実例: ルールの Fix 実装
472
505
 
473
506
  ```typescript
package/ARCHITECTURE.md CHANGED
@@ -53,6 +53,7 @@ src/
53
53
  │ ├── create-rule.ts — createRule factory
54
54
  │ ├── create-test-rule.ts — Test rule factory
55
55
  │ └── types.ts — RuleSeed, Checker types
56
+ ├── cursor-offset.ts — computeCursorOffset (cursor remapping after edits)
56
57
  ├── fix-applier.ts — applyFixes (overlap-aware TextEdit applicator)
57
58
  ├── ruleset/
58
59
  │ └── index.ts — Ruleset class (rules + nodeRules + childNodeRules)
@@ -133,7 +134,7 @@ flowchart LR
133
134
  A["MLCore\nconstructor"]
134
135
  B["_parse()\nParser → MLASTDocument"]
135
136
  C["_createDocument()\nMLASTDocument → MLDocument"]
136
- D["verify(fix?)\nFor each rule:"]
137
+ D["verify(fix? | options?)\nFor each rule:"]
137
138
  E["document.setRule(rule)\nRuleMapper maps config → nodes"]
138
139
  F["rule.verify(document)\nMLRuleContext collects reports"]
139
140
  G["Violations[]"]
@@ -146,7 +147,7 @@ flowchart LR
146
147
  1. **Parse**: `MLCore` invokes the configured parser (`MLParser`) to produce an `MLASTDocument`
147
148
  2. **Create Document**: The AST is wrapped in an `MLDocument`, which builds the full MLDOM tree via `createNode()` factory. `RuleMapper` resolves rule configuration for every node
148
149
  3. **Verify**: For each `MLRule`, the engine calls `document.setRule(rule)` then `rule.verify(document)`. The rule walks relevant nodes via `document.walkOn()` and reports violations through `MLRuleContext`. Rules may attach inline `fix` callbacks to reports that return `TextEdit` objects
149
- 4. **Fix** (optional): When `fix=true`, fix callbacks on reports are executed via `RuleFixer` to produce `TextEdit[]`. `FixApplier.applyFixes(sourceCode, fixes)` applies all edits to the source text with overlap detection
150
+ 4. **Fix** (optional): When `fix=true`, fix callbacks on reports are executed via `RuleFixer` to produce `TextEdit[]`. `FixApplier.applyFixes(sourceCode, fixes)` applies all edits to the source text with overlap detection. When fixes require multiple passes, `_multiPassFix()` orchestrates re-parsing and re-verification, returning a `FixSummary` with pass count, applied/skipped totals, and first-pass edits for cursor offset computation
150
151
 
151
152
  ## MLDOM Class Hierarchy
152
153
 
@@ -423,7 +424,7 @@ sequenceDiagram
423
424
 
424
425
  Core->>Core: Collect all FixData from violations
425
426
  Core->>FA: applyFixes(sourceCode, allFixes)
426
- FA-->>Core: FixResult { output, applied, skipped }
427
+ FA-->>Core: FixResult { output, applied, skipped, appliedEdits }
427
428
  ```
428
429
 
429
430
  ### RuleFixer API
@@ -439,7 +440,7 @@ sequenceDiagram
439
440
  | `remove(token)` | Token with `startOffset` + `raw` | `range: [start, start+len], text: ""` |
440
441
  | `removeRange(range)` | Explicit `[start, end)` range | `range: [start, end], text: ""` |
441
442
 
442
- The `token` parameter accepts any object with `startOffset` and `raw` properties. MLDOM tokens (`MLToken`, `MLAttr`, etc.) satisfy this naturally.
443
+ The `token` parameter accepts any object satisfying the `FixToken` type (defined in `@markuplint/ml-config`) — i.e., `{ startOffset: number; raw: string }`. MLDOM tokens (`MLToken`, `MLAttr`, etc.) satisfy this naturally.
443
444
 
444
445
  ### FixApplier Algorithm
445
446
 
@@ -454,7 +455,7 @@ flowchart TD
454
455
  E["Skip edit\n(mark parent FixData as skipped)"]
455
456
  F["Apply edit\n(splice into output)"]
456
457
  G["Classify: each FixData as\napplied or skipped"]
457
- H["Return FixResult\n{ output, applied, skipped }"]
458
+ H["Return FixResult\n{ output, applied, skipped, appliedEdits }"]
458
459
 
459
460
  A --> B --> C --> D
460
461
  D -- Yes --> E --> C
@@ -467,6 +468,88 @@ Key constraints:
467
468
  - Edits within a single `FixData` must not overlap each other
468
469
  - Inter-`FixData` overlap is handled by the skip mechanism
469
470
  - If any edit in a `FixData` is skipped, the entire `FixData` is classified as skipped
471
+ - `appliedEdits` is a flat list of all successfully applied `TextEdit` objects, sorted by `range[0]` ascending — used for cursor offset computation
472
+
473
+ ### Multi-Pass Fix Loop
474
+
475
+ When `applyFixes()` skips some fixes due to range overlap, the engine enters a multi-pass loop (`_multiPassFix()`) that re-parses and re-verifies until all fixable violations are resolved:
476
+
477
+ ```mermaid
478
+ flowchart TD
479
+ A["Extract fixes from violations"] --> B["applyFixes(code, fixes)"]
480
+ B --> C{"applied.length === 0?"}
481
+ C -- Yes --> Z["Return current code"]
482
+ C -- No --> D{"output === currentCode?"}
483
+ D -- Yes --> Z
484
+ D -- No --> E{"Cycle detected?\n(output === code from 2 passes ago)"}
485
+ E -- Yes --> Z
486
+ E -- No --> F{"skipped.length === 0?"}
487
+ F -- Yes --> Z["Return fixed code\n(all fixes applied)"]
488
+ F -- No --> G["Re-parse + re-verify"]
489
+ G --> H{"ParserError?"}
490
+ H -- Yes --> Z["Revert to last good code"]
491
+ H -- No --> I{"New fixable violations?"}
492
+ I -- No --> Z
493
+ I -- Yes --> B
494
+ ```
495
+
496
+ Key design points:
497
+
498
+ - **Zero-cost path**: If no violations have fixes, the multi-pass loop is skipped entirely
499
+ - **Single-pass fast path**: When `skipped.length === 0`, the loop exits immediately — equivalent to Phase 1 behavior
500
+ - **Cycle detection**: Compares current output against the output from two passes ago to detect A→B→A oscillation
501
+ - **Safety cap**: Maximum 10 passes (same as ESLint's `SourceCodeFixer`)
502
+ - **State restoration**: `verify()` saves and restores `#sourceCode`, `#ast`, and `#document` via `try/finally`
503
+
504
+ **Important**: The `violations` array in `VerifyResult` reflects the first pass only, while `fixedCode` may be the result of multiple passes. Callers needing an accurate violation list for the fixed code should re-verify the output.
505
+
506
+ ### VerifyResult and FixSummary
507
+
508
+ `MLCore.verify()` accepts either a `boolean` or a `VerifyOptions` object:
509
+
510
+ ```typescript
511
+ verify(fix?: boolean): Promise<VerifyResult>;
512
+ verify(options?: VerifyOptions): Promise<VerifyResult>;
513
+ ```
514
+
515
+ `VerifyResult` contains:
516
+
517
+ | Field | Type | Description |
518
+ | ------------ | ------------------------- | ------------------------------------------------------------- |
519
+ | `violations` | `readonly Violation[]` | Violations from the first verification pass |
520
+ | `fixedCode` | `string \| undefined` | Source after all fix passes; `undefined` when fix is disabled |
521
+ | `fixSummary` | `FixSummary \| undefined` | Fix process summary; present when `fix=true` |
522
+
523
+ `FixSummary` provides diagnostics about the multi-pass fix process:
524
+
525
+ | Field | Type | Description |
526
+ | ------------------ | --------------------- | ---------------------------------------------------------------- |
527
+ | `passCount` | `number` | Number of fix passes executed |
528
+ | `totalApplied` | `number` | Total fixes applied across all passes |
529
+ | `totalSkipped` | `number` | Total fixes skipped (overlap) across all passes |
530
+ | `reachedMaxPasses` | `boolean` | Whether the 10-pass safety cap was reached |
531
+ | `firstPassEdits` | `readonly TextEdit[]` | Applied edits from the first pass only (original source offsets) |
532
+
533
+ `firstPassEdits` references the original source code offsets, making them suitable for cursor remapping via `computeCursorOffset()`.
534
+
535
+ ### Cursor Offset Computation
536
+
537
+ `computeCursorOffset()` (in `cursor-offset.ts`) maps a cursor position from the original source to the fixed source using the first-pass applied edits:
538
+
539
+ ```typescript
540
+ import { computeCursorOffset } from '@markuplint/ml-core';
541
+
542
+ const newOffset = computeCursorOffset(fixSummary.firstPassEdits, originalCursorOffset);
543
+ ```
544
+
545
+ Algorithm:
546
+
547
+ 1. Walk through edits sorted by `range[0]` ascending
548
+ 2. For each edit before the cursor: accumulate `delta = text.length - (end - start)`
549
+ 3. For edits after the cursor: stop (no effect)
550
+ 4. If the cursor falls inside a replaced range `[start, end)`: place at `start + text.length`
551
+
552
+ Ranges use half-open intervals: a cursor at position `end` is considered **outside** the edit.
470
553
 
471
554
  ### Example: Rule Fix in Practice
472
555
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
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-alpha.3](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.2...v5.0.0-alpha.3) (2026-02-26)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** treat edits within a single FixData as atomic unit ([0bb980b](https://github.com/markuplint/markuplint/commit/0bb980b7cc6fc9b89a82f3d4df58b7137a6b8766))
11
+
12
+ ### Features
13
+
14
+ - **ml-core:** add cursor offset computation and fix summary metadata ([74b6e28](https://github.com/markuplint/markuplint/commit/74b6e28e4be2802e841697899f57f6ae04e4ffe9))
15
+ - **ml-core:** add multi-pass fix loop and cycle detection ([866b1d5](https://github.com/markuplint/markuplint/commit/866b1d54199ed1f1b5195cd0f61f3ee392b1d8a7))
16
+
6
17
  # [5.0.0-alpha.2](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.1...v5.0.0-alpha.2) (2026-02-23)
7
18
 
8
19
  ### Features
@@ -0,0 +1,13 @@
1
+ import type { TextEdit } from '@markuplint/ml-config';
2
+ /**
3
+ * Computes the new cursor offset after text edits have been applied.
4
+ *
5
+ * For each edit before the cursor: delta += text.length - (end - start).
6
+ * If the cursor falls inside a replaced range [start, end), it is placed
7
+ * at start + text.length (immediately after the replacement).
8
+ *
9
+ * @param appliedEdits - Applied edits sorted by range[0] ascending
10
+ * @param cursorOffset - Original 0-based cursor offset
11
+ * @returns New cursor offset in the fixed code
12
+ */
13
+ export declare function computeCursorOffset(appliedEdits: readonly TextEdit[], cursorOffset: number): number;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Computes the new cursor offset after text edits have been applied.
3
+ *
4
+ * For each edit before the cursor: delta += text.length - (end - start).
5
+ * If the cursor falls inside a replaced range [start, end), it is placed
6
+ * at start + text.length (immediately after the replacement).
7
+ *
8
+ * @param appliedEdits - Applied edits sorted by range[0] ascending
9
+ * @param cursorOffset - Original 0-based cursor offset
10
+ * @returns New cursor offset in the fixed code
11
+ */
12
+ export function computeCursorOffset(appliedEdits, cursorOffset) {
13
+ let newOffset = cursorOffset;
14
+ for (const edit of appliedEdits) {
15
+ const [start, end] = edit.range;
16
+ const delta = edit.text.length - (end - start);
17
+ if (start > cursorOffset) {
18
+ // Edit is after cursor — no effect
19
+ break;
20
+ }
21
+ if (end <= cursorOffset) {
22
+ // Edit is entirely before cursor — shift by delta.
23
+ // Range is half-open [start, end), so cursor at `end` is outside the edit.
24
+ newOffset += delta;
25
+ }
26
+ else {
27
+ // Cursor falls inside the replaced range [start, end)
28
+ newOffset = start + edit.text.length;
29
+ break;
30
+ }
31
+ }
32
+ // Defensive guard: should never go negative with well-formed, non-overlapping edits
33
+ return Math.max(0, newOffset);
34
+ }
@@ -1,4 +1,4 @@
1
- import type { FixData } from '@markuplint/ml-config';
1
+ import type { FixData, TextEdit } from '@markuplint/ml-config';
2
2
  /**
3
3
  * The result of applying fixes to source code.
4
4
  */
@@ -9,6 +9,8 @@ export type FixResult = {
9
9
  readonly applied: readonly FixData[];
10
10
  /** Fixes that were skipped due to overlapping ranges */
11
11
  readonly skipped: readonly FixData[];
12
+ /** Flat list of successfully applied edits, sorted by range[0] ascending */
13
+ readonly appliedEdits: readonly TextEdit[];
12
14
  };
13
15
  /**
14
16
  * Applies a set of text edits to the source code.
@@ -17,7 +17,7 @@
17
17
  */
18
18
  export function applyFixes(sourceCode, fixes) {
19
19
  if (fixes.length === 0) {
20
- return { output: sourceCode, applied: [], skipped: [] };
20
+ return { output: sourceCode, applied: [], skipped: [], appliedEdits: [] };
21
21
  }
22
22
  // Tag each edit with its parent FixData index
23
23
  const taggedEdits = [];
@@ -34,18 +34,22 @@ export function applyFixes(sourceCode, fixes) {
34
34
  });
35
35
  // Track which FixData indices had at least one skipped edit
36
36
  const skippedFixIndices = new Set();
37
+ const appliedEdits = [];
37
38
  let lastAppliedEnd = -1;
38
39
  const parts = [];
39
40
  let cursor = 0;
40
41
  for (const { edit, fixIndex } of taggedEdits) {
41
42
  const [start, end] = edit.range;
42
- // Overlap check: if this edit starts before the end of the last applied edit, skip it
43
- if (start < lastAppliedEnd) {
43
+ // Overlap check: if this edit starts before the end of the last applied edit, skip it.
44
+ // Also skip if a sibling edit from the same FixData was already skipped —
45
+ // edits within a single FixData are atomic (all-or-nothing).
46
+ if (start < lastAppliedEnd || skippedFixIndices.has(fixIndex)) {
44
47
  skippedFixIndices.add(fixIndex);
45
48
  continue;
46
49
  }
47
50
  // Append the source text between the last edit and this one
48
51
  parts.push(sourceCode.slice(cursor, start), edit.text);
52
+ appliedEdits.push(edit);
49
53
  cursor = end;
50
54
  lastAppliedEnd = end;
51
55
  }
@@ -66,5 +70,6 @@ export function applyFixes(sourceCode, fixes) {
66
70
  output: parts.join(''),
67
71
  applied,
68
72
  skipped,
73
+ appliedEdits,
69
74
  };
70
75
  }
package/lib/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export type { RuleInfo, RuleConfig, RuleConfigValue } from '@markuplint/ml-confi
2
2
  export { ariaSpecs, contentModelCategoryToTagNames, getAttrSpecs, getComputedRole, getImplicitRole, getPermittedRoles, getRoleSpec, getSpec, resolveNamespace, } from '@markuplint/ml-spec';
3
3
  export { Ruleset } from './ruleset/index.js';
4
4
  export { enableDebug } from './debug.js';
5
+ export { computeCursorOffset } from './cursor-offset.js';
5
6
  export { applyFixes } from './fix-applier.js';
6
7
  export type { FixResult } from './fix-applier.js';
7
8
  export { getIndent } from './ml-dom/helper/get-indent.js';
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { ariaSpecs, contentModelCategoryToTagNames, getAttrSpecs, getComputedRole, getImplicitRole, getPermittedRoles, getRoleSpec, getSpec, resolveNamespace, } from '@markuplint/ml-spec';
2
2
  export { Ruleset } from './ruleset/index.js';
3
3
  export { enableDebug } from './debug.js';
4
+ export { computeCursorOffset } from './cursor-offset.js';
4
5
  export { applyFixes } from './fix-applier.js';
5
6
  export { getIndent } from './ml-dom/helper/get-indent.js';
6
7
  export * from './convert-ruleset.js';
package/lib/ml-core.d.ts CHANGED
@@ -1,7 +1,34 @@
1
1
  import type { MLFabric } from './types.js';
2
- import type { PlainData, RuleConfigValue, Violation } from '@markuplint/ml-config';
2
+ import type { PlainData, RuleConfigValue, TextEdit, Violation } from '@markuplint/ml-config';
3
3
  import { ParserError } from '@markuplint/parser-utils';
4
4
  import { Document } from './ml-dom/index.js';
5
+ /**
6
+ * Summary of the multi-pass fix process.
7
+ */
8
+ export type FixSummary = {
9
+ /** Number of fix passes executed (i.e., the number of times applyFixes was called) */
10
+ readonly passCount: number;
11
+ /** Total fixes applied across all passes */
12
+ readonly totalApplied: number;
13
+ /** Total fixes skipped (overlap) across all passes */
14
+ readonly totalSkipped: number;
15
+ /** Whether the maximum pass count was reached */
16
+ readonly reachedMaxPasses: boolean;
17
+ /**
18
+ * Applied edits from the FIRST pass only.
19
+ * These reference the original source code offsets, making them suitable
20
+ * for cursor offset computation via {@link computeCursorOffset}.
21
+ * Note: `firstPassEdits.length` may differ from `totalApplied` when
22
+ * multiple passes are executed.
23
+ */
24
+ readonly firstPassEdits: readonly TextEdit[];
25
+ };
26
+ /**
27
+ * Options for {@link MLCore.verify}.
28
+ */
29
+ export type VerifyOptions = {
30
+ readonly fix?: boolean;
31
+ };
5
32
  /**
6
33
  * The result of running {@link MLCore.verify}.
7
34
  */
@@ -10,6 +37,8 @@ export type VerifyResult = {
10
37
  readonly violations: readonly Violation[];
11
38
  /** The source code after applying fixes. `undefined` when fix is not enabled. */
12
39
  readonly fixedCode: string | undefined;
40
+ /** Fix process summary. Present when fix=true. */
41
+ readonly fixSummary?: FixSummary;
13
42
  };
14
43
  /**
15
44
  * Parameters for constructing an {@link MLCore} instance.
@@ -35,7 +64,7 @@ export declare class MLCore {
35
64
  /**
36
65
  * The parsed document, or a {@link ParserError} if parsing failed.
37
66
  */
38
- get document(): ParserError | Document<RuleConfigValue, PlainData>;
67
+ get document(): Document<RuleConfigValue, PlainData> | ParserError;
39
68
  /**
40
69
  * Replaces the source code and re-parses the document.
41
70
  *
@@ -56,13 +85,17 @@ export declare class MLCore {
56
85
  * (unless parse errors are suppressed via severity options).
57
86
  *
58
87
  * When `fix` is true, fix callbacks are executed and the resulting TextEdits
59
- * are applied to produce `fixedCode`.
88
+ * are applied via a multi-pass loop to produce `fixedCode`.
89
+ *
90
+ * **Important**: `violations` reflects the *first* pass only, while `fixedCode`
91
+ * may be the result of multiple fix passes. This means some violations in the
92
+ * array may already be resolved in `fixedCode`, and new violations introduced
93
+ * during later passes are not included in the array. Callers needing an accurate
94
+ * violation list for the fixed code should re-verify the output.
60
95
  *
61
- * @param fix - Whether to attempt auto-fixing violations
62
- * @returns Violations and the (possibly fixed) source code
96
+ * @param fixOrOptions - Whether to attempt auto-fixing violations, or an options object
97
+ * @returns Violations from the initial analysis and the (possibly fixed) source code
63
98
  */
64
99
  verify(fix?: boolean): Promise<VerifyResult>;
65
- private _createDocument;
66
- private _createParseError;
67
- private _parse;
100
+ verify(options?: VerifyOptions): Promise<VerifyResult>;
68
101
  }
package/lib/ml-core.js CHANGED
@@ -70,8 +70,8 @@ export class MLCore {
70
70
  };
71
71
  this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
72
72
  this.#configErrors.push(...namedRulesResult.errors, ...nodeRuleResult.errors, ...childNodeRuleResult.errors);
73
- this._parse();
74
- this._createDocument();
73
+ this.#parse();
74
+ this.#createDocument();
75
75
  }
76
76
  /**
77
77
  * The parsed document, or a {@link ParserError} if parsing failed.
@@ -86,8 +86,8 @@ export class MLCore {
86
86
  */
87
87
  setCode(sourceCode) {
88
88
  this.#sourceCode = sourceCode;
89
- this._parse();
90
- this._createDocument();
89
+ this.#parse();
90
+ this.#createDocument();
91
91
  }
92
92
  /**
93
93
  * Updates the linting configuration and re-creates the document.
@@ -129,27 +129,17 @@ export class MLCore {
129
129
  if (parserOptions &&
130
130
  (parserOptions.ignoreFrontMatter !== this.#parserOptions.ignoreFrontMatter ||
131
131
  parserOptions.authoredElementName !== this.#parserOptions.authoredElementName)) {
132
- this._parse();
132
+ this.#parse();
133
133
  }
134
- this._createDocument();
134
+ this.#createDocument();
135
135
  }
136
- /**
137
- * Runs all configured rules against the parsed document and returns violations.
138
- *
139
- * If the document failed to parse, a single parse-error violation is returned
140
- * (unless parse errors are suppressed via severity options).
141
- *
142
- * When `fix` is true, fix callbacks are executed and the resulting TextEdits
143
- * are applied to produce `fixedCode`.
144
- *
145
- * @param fix - Whether to attempt auto-fixing violations
146
- * @returns Violations and the (possibly fixed) source code
147
- */
148
- async verify(fix = false) {
136
+ async verify(fixOrOptions) {
137
+ const options = typeof fixOrOptions === 'boolean' ? { fix: fixOrOptions } : (fixOrOptions ?? {});
138
+ const fix = options.fix ?? false;
149
139
  log('verify: start');
150
140
  const violations = [];
151
141
  if (this.#document instanceof ParserError) {
152
- const parseError = this._createParseError(this.#document.message, this.#document.line, this.#document.col, this.#document.raw);
142
+ const parseError = this.#createParseError(this.#document.message, this.#document.line, this.#document.col, this.#document.raw);
153
143
  if (!parseError) {
154
144
  return { violations: [], fixedCode: fix ? this.#sourceCode : undefined };
155
145
  }
@@ -189,42 +179,8 @@ export class MLCore {
189
179
  raw: '',
190
180
  });
191
181
  }
192
- for (const rule of this.#rules) {
193
- // For virtual rules, check disable conditions:
194
- // 1. Exact name match: rules["alias/name"]: false
195
- // 2. Group disable: rules["groupName"]: false (multi-entry named nodeRules)
196
- // 3. Namespace wildcard: rules["scope/*"]: false
197
- // Note: base rule name disable (rules["baseRuleName"]: false) is handled
198
- // during expandNamedRules for named rule groups in the rules section.
199
- if (rule.baseRuleId &&
200
- (this.#ruleset.rules[rule.name] === false ||
201
- (rule.groupName && this.#ruleset.rules[rule.groupName] === false) ||
202
- this.#disabledNamespaces.some(ns => rule.name.startsWith(ns)))) {
203
- continue;
204
- }
205
- const ruleInfo = rule.getRuleInfo(this.#ruleset, rule.name);
206
- if (ruleInfo.disabled && ruleInfo.nodeRules.length === 0 && ruleInfo.childNodeRules.length === 0) {
207
- continue;
208
- }
209
- log('%s Rule: verify', rule.name);
210
- const results = await rule.verify(this.#document, this.#locale, fix).catch(error => {
211
- if (error instanceof ParserError) {
212
- return error;
213
- }
214
- throw error;
215
- });
216
- if (results instanceof ParserError) {
217
- const parseError = this._createParseError(results.message, results.line, results.col, results.raw);
218
- if (parseError) {
219
- log('%s Rule: verify error %o', rule.name, results.message);
220
- violations.push(parseError);
221
- }
222
- }
223
- else {
224
- violations.push(...results);
225
- }
226
- log('%s Rule: verify end', rule.name);
227
- }
182
+ const ruleViolations = await this.#runAllRules(fix);
183
+ violations.push(...ruleViolations);
228
184
  if (resultLog.enabled) {
229
185
  // eslint-disable-next-line unicorn/no-array-reduce
230
186
  const { e, w, i } = violations.reduce((c, v) => {
@@ -242,25 +198,40 @@ export class MLCore {
242
198
  }
243
199
  // Apply fixes if enabled
244
200
  let fixedCode;
201
+ let fixSummary;
245
202
  if (fix) {
246
- fixedCode = this.#sourceCode;
247
- const allFixes = [];
248
- for (const v of violations) {
249
- if (v.fix) {
250
- allFixes.push(v.fix);
203
+ const hasFixes = violations.some(v => v.fix);
204
+ if (hasFixes) {
205
+ const originalSourceCode = this.#sourceCode;
206
+ const originalAst = this.#ast;
207
+ const originalDocument = this.#document;
208
+ try {
209
+ const fixResult = await this.#multiPassFix(violations);
210
+ fixedCode = fixResult.code;
211
+ fixSummary = fixResult.summary;
212
+ }
213
+ finally {
214
+ // Restore original state - verify() must be non-mutating
215
+ this.#sourceCode = originalSourceCode;
216
+ this.#ast = originalAst;
217
+ this.#document = originalDocument;
251
218
  }
252
219
  }
253
- if (allFixes.length > 0) {
254
- const result = applyFixes(this.#sourceCode, allFixes);
255
- fixedCode = result.output;
256
- // TODO(Phase 2): If skipped fixes exist, implement multi-pass re-verify loop
257
- // (similar to ESLint's 10-pass fix loop) to resolve conflicts iteratively.
220
+ else {
221
+ fixedCode = this.#sourceCode;
222
+ fixSummary = {
223
+ passCount: 0,
224
+ totalApplied: 0,
225
+ totalSkipped: 0,
226
+ reachedMaxPasses: false,
227
+ firstPassEdits: [],
228
+ };
258
229
  }
259
230
  }
260
231
  log('verify: end');
261
- return { violations, fixedCode };
232
+ return { violations, fixedCode, fixSummary };
262
233
  }
263
- _createDocument() {
234
+ #createDocument() {
264
235
  if (!this.#ast) {
265
236
  return;
266
237
  }
@@ -282,7 +253,7 @@ export class MLCore {
282
253
  }
283
254
  }
284
255
  }
285
- _createParseError(message, line, col, raw) {
256
+ #createParseError(message, line, col, raw) {
286
257
  if (this.#severity.parseError === false || this.#severity.parseError === 'off') {
287
258
  return null;
288
259
  }
@@ -299,7 +270,138 @@ export class MLCore {
299
270
  raw,
300
271
  };
301
272
  }
302
- _parse() {
273
+ /**
274
+ * Iteratively applies fixes, re-parses, and re-verifies until no overlapping
275
+ * fixes remain or the maximum pass count is reached (ESLint-style multi-pass loop).
276
+ *
277
+ * **Callers must save/restore `#sourceCode`, `#ast`, and `#document`** because
278
+ * this method mutates them during intermediate re-parse steps.
279
+ *
280
+ * @param initialViolations - Violations from the first verification pass
281
+ * @returns The final fixed source code and a summary of the fix process
282
+ */
283
+ async #multiPassFix(initialViolations) {
284
+ const MAX_FIX_PASSES = 10;
285
+ let currentCode = this.#sourceCode;
286
+ let previousCode;
287
+ let fixes = extractFixes(initialViolations);
288
+ let totalApplied = 0;
289
+ let totalSkipped = 0;
290
+ let firstPassEdits = [];
291
+ let pass = 0;
292
+ for (; pass < MAX_FIX_PASSES; pass++) {
293
+ log('fix pass %d: %d fixes', pass, fixes.length);
294
+ const result = applyFixes(currentCode, fixes);
295
+ totalApplied += result.applied.length;
296
+ totalSkipped += result.skipped.length;
297
+ if (pass === 0) {
298
+ firstPassEdits = result.appliedEdits;
299
+ }
300
+ if (result.applied.length === 0) {
301
+ log('fix pass %d: no fixes applied, stopping', pass);
302
+ break;
303
+ }
304
+ if (result.output === currentCode) {
305
+ log('fix pass %d: output unchanged, stopping', pass);
306
+ break;
307
+ }
308
+ // Cycle detection: if the output matches the code from two passes ago,
309
+ // fixes are oscillating (A → B → A) and will never converge.
310
+ if (previousCode !== undefined && result.output === previousCode) {
311
+ log('fix pass %d: cycle detected (output matches pass %d), stopping', pass, pass - 2);
312
+ currentCode = result.output;
313
+ break;
314
+ }
315
+ previousCode = currentCode;
316
+ currentCode = result.output;
317
+ if (result.skipped.length === 0) {
318
+ log('fix pass %d: all fixes applied, stopping', pass);
319
+ break;
320
+ }
321
+ // --- Multi-pass path (only when overlapping fixes exist) ---
322
+ log('fix pass %d: %d skipped, re-parsing for next pass', pass, result.skipped.length);
323
+ const previousGoodCode = currentCode;
324
+ this.#sourceCode = currentCode;
325
+ this.#parse();
326
+ this.#createDocument();
327
+ if (this.#document instanceof ParserError) {
328
+ log('fix pass %d: produced unparsable code, reverting to previous state', pass);
329
+ currentCode = previousGoodCode;
330
+ break;
331
+ }
332
+ const newViolations = await this.#runAllRules(true);
333
+ fixes = extractFixes(newViolations);
334
+ if (fixes.length === 0) {
335
+ log('fix pass %d: no more fixable violations, stopping', pass);
336
+ break;
337
+ }
338
+ }
339
+ const reachedMaxPasses = pass === MAX_FIX_PASSES;
340
+ if (reachedMaxPasses) {
341
+ log('fix: reached maximum number of passes (%d), some fixes may not have been applied', MAX_FIX_PASSES);
342
+ }
343
+ return {
344
+ code: currentCode,
345
+ summary: {
346
+ passCount: Math.min(pass + 1, MAX_FIX_PASSES),
347
+ totalApplied,
348
+ totalSkipped,
349
+ reachedMaxPasses,
350
+ firstPassEdits,
351
+ },
352
+ };
353
+ }
354
+ /**
355
+ * Executes all configured rules against the current document and collects violations.
356
+ * Skips disabled rules and handles virtual rule disable conditions.
357
+ *
358
+ * @param fix - Whether to execute fix callbacks on violations
359
+ * @returns All violations produced by the rule set
360
+ */
361
+ async #runAllRules(fix) {
362
+ const violations = [];
363
+ if (this.#document instanceof ParserError) {
364
+ return violations;
365
+ }
366
+ for (const rule of this.#rules) {
367
+ // For virtual rules, check disable conditions:
368
+ // 1. Exact name match: rules["alias/name"]: false
369
+ // 2. Group disable: rules["groupName"]: false (multi-entry named nodeRules)
370
+ // 3. Namespace wildcard: rules["scope/*"]: false
371
+ // Note: base rule name disable (rules["baseRuleName"]: false) is handled
372
+ // during expandNamedRules for named rule groups in the rules section.
373
+ if (rule.baseRuleId &&
374
+ (this.#ruleset.rules[rule.name] === false ||
375
+ (rule.groupName && this.#ruleset.rules[rule.groupName] === false) ||
376
+ this.#disabledNamespaces.some(ns => rule.name.startsWith(ns)))) {
377
+ continue;
378
+ }
379
+ const ruleInfo = rule.getRuleInfo(this.#ruleset, rule.name);
380
+ if (ruleInfo.disabled && ruleInfo.nodeRules.length === 0 && ruleInfo.childNodeRules.length === 0) {
381
+ continue;
382
+ }
383
+ log('%s Rule: verify', rule.name);
384
+ const results = await rule.verify(this.#document, this.#locale, fix).catch(error => {
385
+ if (error instanceof ParserError) {
386
+ return error;
387
+ }
388
+ throw error;
389
+ });
390
+ if (results instanceof ParserError) {
391
+ const parseError = this.#createParseError(results.message, results.line, results.col, results.raw);
392
+ if (parseError) {
393
+ log('%s Rule: verify error %o', rule.name, results.message);
394
+ violations.push(parseError);
395
+ }
396
+ }
397
+ else {
398
+ violations.push(...results);
399
+ }
400
+ log('%s Rule: verify end', rule.name);
401
+ }
402
+ return violations;
403
+ }
404
+ #parse() {
303
405
  try {
304
406
  this.#ast = this.#parser.parse(this.#sourceCode, this.#parserOptions);
305
407
  }
@@ -324,3 +426,18 @@ function extractDisabledNamespaces(rules) {
324
426
  .filter(([key, value]) => key.endsWith('/*') && value === false)
325
427
  .map(([key]) => key.slice(0, -1)); // "a11y/*" → "a11y/"
326
428
  }
429
+ /**
430
+ * Collects all `FixData` from violations that have a fix callback result.
431
+ *
432
+ * @param violations - The violations to extract fixes from
433
+ * @returns An array of `FixData` objects ready for `applyFixes()`
434
+ */
435
+ function extractFixes(violations) {
436
+ const fixes = [];
437
+ for (const v of violations) {
438
+ if (v.fix) {
439
+ fixes.push(v.fix);
440
+ }
441
+ }
442
+ return fixes;
443
+ }
@@ -28,7 +28,8 @@ export class MLBlock extends MLNode {
28
28
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
29
29
  document) {
30
30
  super(astNode, document, astNode.isFragment);
31
- // TODO:
31
+ // Always transparent: blockBehavior may restrict child treatment in the future,
32
+ // but currently all preprocessor blocks are transparent for tree traversal.
32
33
  this.isTransparent = true;
33
34
  this.blockBehavior = astNode.blockBehavior;
34
35
  }
@@ -42,21 +42,56 @@ export declare abstract class MLCharacterData<T extends RuleConfigValue, O exten
42
42
  * @implements DOM API: `CharacterData`
43
43
  */
44
44
  after(...nodes: (string | MLElement<any, any>)[]): void;
45
+ /**
46
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
47
+ *
48
+ * @unsupported
49
+ * @implements DOM API: `CharacterData`
50
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-appenddata
51
+ */
45
52
  appendData(data: string): void;
46
53
  /**
47
54
  * @implements DOM API: `CharacterData`
48
55
  */
49
56
  before(...nodes: (string | MLElement<any, any>)[]): void;
57
+ /**
58
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
59
+ *
60
+ * @unsupported
61
+ * @implements DOM API: `CharacterData`
62
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-deletedata
63
+ */
50
64
  deleteData(offset: number, count: number): void;
65
+ /**
66
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
67
+ *
68
+ * @unsupported
69
+ * @implements DOM API: `CharacterData`
70
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-insertdata
71
+ */
51
72
  insertData(offset: number, data: string): void;
52
73
  /**
53
74
  * @implements DOM API: `CharacterData`
54
75
  */
55
76
  remove(): void;
77
+ /**
78
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
79
+ *
80
+ * @unsupported
81
+ * @implements DOM API: `CharacterData`
82
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-replacedata
83
+ */
56
84
  replaceData(offset: number, count: number, data: string): void;
57
85
  /**
58
86
  * @implements DOM API: `CharacterData`
59
87
  */
60
88
  replaceWith(...nodes: (string | MLElement<any, any>)[]): void;
89
+ /**
90
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
91
+ *
92
+ * @unsupported
93
+ * @implements DOM API: `CharacterData`
94
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-substringdata
95
+ */
61
96
  substringData(offset: number, count: number): string;
62
97
  }
@@ -7,7 +7,6 @@ export class MLCharacterData extends MLNode {
7
7
  * @see https://dom.spec.whatwg.org/#dom-characterdata-data
8
8
  */
9
9
  get data() {
10
- // TODO:
11
10
  return this.raw;
12
11
  }
13
12
  /**
@@ -58,7 +57,13 @@ export class MLCharacterData extends MLNode {
58
57
  ...nodes) {
59
58
  after(this, ...nodes);
60
59
  }
61
- // TODO
60
+ /**
61
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
62
+ *
63
+ * @unsupported
64
+ * @implements DOM API: `CharacterData`
65
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-appenddata
66
+ */
62
67
  appendData(data) { }
63
68
  /**
64
69
  * @implements DOM API: `CharacterData`
@@ -68,9 +73,21 @@ export class MLCharacterData extends MLNode {
68
73
  ...nodes) {
69
74
  before(this, ...nodes);
70
75
  }
71
- // TODO
76
+ /**
77
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
78
+ *
79
+ * @unsupported
80
+ * @implements DOM API: `CharacterData`
81
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-deletedata
82
+ */
72
83
  deleteData(offset, count) { }
73
- // TODO
84
+ /**
85
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
86
+ *
87
+ * @unsupported
88
+ * @implements DOM API: `CharacterData`
89
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-insertdata
90
+ */
74
91
  insertData(offset, data) { }
75
92
  /**
76
93
  * @implements DOM API: `CharacterData`
@@ -78,7 +95,13 @@ export class MLCharacterData extends MLNode {
78
95
  remove() {
79
96
  remove(this);
80
97
  }
81
- // TODO
98
+ /**
99
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
100
+ *
101
+ * @unsupported
102
+ * @implements DOM API: `CharacterData`
103
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-replacedata
104
+ */
82
105
  replaceData(offset, count, data) { }
83
106
  /**
84
107
  * @implements DOM API: `CharacterData`
@@ -88,7 +111,13 @@ export class MLCharacterData extends MLNode {
88
111
  ...nodes) {
89
112
  replaceWith(this, ...nodes);
90
113
  }
91
- // TODO
114
+ /**
115
+ * **IT THROWS AN ERROR WHEN CALLING THIS.**
116
+ *
117
+ * @unsupported
118
+ * @implements DOM API: `CharacterData`
119
+ * @see https://dom.spec.whatwg.org/#dom-characterdata-substringdata
120
+ */
92
121
  substringData(offset, count) {
93
122
  return '';
94
123
  }
@@ -1721,18 +1721,4 @@ export declare class MLDocument<T extends RuleConfigValue, O extends PlainData =
1721
1721
  * @implements DOM API: `Document`
1722
1722
  */
1723
1723
  writeln(...text: readonly string[]): void;
1724
- /**
1725
- * Initializes pretender contexts for all element nodes in the document.
1726
- *
1727
- * @param pretenders - Optional pretender configurations from the document options
1728
- */
1729
- private _pretending;
1730
- /**
1731
- * Maps the ruleset configuration to each node in the document.
1732
- * Applies global rules, node-specific rules (by selector), and
1733
- * child-node rules to build the per-node rule configuration.
1734
- *
1735
- * @param ruleset - The ruleset containing rules, nodeRules, and childNodeRules
1736
- */
1737
- private _ruleMapping;
1738
1724
  }
@@ -107,9 +107,9 @@ export class MLDocument extends MLParentNode {
107
107
  return createNode(astNode, this);
108
108
  })
109
109
  .filter((n) => !!n));
110
- this._pretending(options?.pretenders);
110
+ this.#pretending(options?.pretenders);
111
111
  try {
112
- this._ruleMapping(ruleset);
112
+ this.#ruleMapping(ruleset);
113
113
  }
114
114
  catch (error) {
115
115
  if (error instanceof InvalidSelectorError) {
@@ -2039,7 +2039,6 @@ export class MLDocument extends MLParentNode {
2039
2039
  * @implements DOM API: `Document`
2040
2040
  */
2041
2041
  getElementById(elementId) {
2042
- // TODO:
2043
2042
  return this.querySelector(`#${elementId}`);
2044
2043
  }
2045
2044
  /**
@@ -2281,7 +2280,7 @@ export class MLDocument extends MLParentNode {
2281
2280
  *
2282
2281
  * @param pretenders - Optional pretender configurations from the document options
2283
2282
  */
2284
- _pretending(pretenders) {
2283
+ #pretending(pretenders) {
2285
2284
  if (docLog.enabled) {
2286
2285
  docLog('Pretending: %O', pretenders);
2287
2286
  }
@@ -2298,7 +2297,7 @@ export class MLDocument extends MLParentNode {
2298
2297
  *
2299
2298
  * @param ruleset - The ruleset containing rules, nodeRules, and childNodeRules
2300
2299
  */
2301
- _ruleMapping(ruleset) {
2300
+ #ruleMapping(ruleset) {
2302
2301
  if (docLog.enabled) {
2303
2302
  docLog('Rule Mapping: %O', Object.keys(ruleset.rules));
2304
2303
  }
@@ -28,6 +28,5 @@ export declare class MLDomTokenList extends Array<string> implements DOMTokenLis
28
28
  supports(token: string): boolean;
29
29
  toString(): string;
30
30
  toggle(token: string, force?: boolean): boolean;
31
- private _pick;
32
31
  }
33
32
  export {};
@@ -45,7 +45,7 @@ export class MLDomTokenList extends Array {
45
45
  if (!token) {
46
46
  break;
47
47
  }
48
- const loc = this._pick(token, offset);
48
+ const loc = this.#pick(token, offset);
49
49
  if (!loc) {
50
50
  offset = 0;
51
51
  continue;
@@ -74,7 +74,7 @@ export class MLDomTokenList extends Array {
74
74
  * @implements `@markuplint/ml-core` API: `MLDomTokenList`
75
75
  */
76
76
  pick(token) {
77
- const r = this._pick(token);
77
+ const r = this.#pick(token);
78
78
  if (!r) {
79
79
  return null;
80
80
  }
@@ -101,7 +101,7 @@ export class MLDomTokenList extends Array {
101
101
  toggle(token, force) {
102
102
  throw new UnexpectedCallError('Not supported "toggle" method');
103
103
  }
104
- _pick(token, _offset = 0) {
104
+ #pick(token, _offset = 0) {
105
105
  token = token.trim().split(/\s+/)[0] ?? '';
106
106
  if (!token) {
107
107
  return null;
@@ -2725,7 +2725,6 @@ export class MLElement extends MLParentNode {
2725
2725
  insertAdjacentElement(where,
2726
2726
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2727
2727
  element) {
2728
- // TODO:
2729
2728
  throw new UnexpectedCallError('Does not implement "insertAdjacentElement" method yet');
2730
2729
  }
2731
2730
  /**
@@ -2736,7 +2735,6 @@ export class MLElement extends MLParentNode {
2736
2735
  * @see https://w3c.github.io/DOM-Parsing/#widl-Element-insertAdjacentHTML-void-DOMString-position-DOMString-text
2737
2736
  */
2738
2737
  insertAdjacentHTML(position, text) {
2739
- // TODO:
2740
2738
  throw new UnexpectedCallError('Does not implement "insertAdjacentHTML" method yet');
2741
2739
  }
2742
2740
  /**
@@ -2747,7 +2745,6 @@ export class MLElement extends MLParentNode {
2747
2745
  * @see https://dom.spec.whatwg.org/#dom-element-insertadjacenttext
2748
2746
  */
2749
2747
  insertAdjacentText(where, data) {
2750
- // TODO:
2751
2748
  throw new UnexpectedCallError('Does not implement "insertAdjacentText" method yet');
2752
2749
  }
2753
2750
  /**
@@ -21,5 +21,4 @@ export declare class MLRuleContext<T extends RuleConfigValue, O extends PlainDat
21
21
  };
22
22
  report(report: Report<T, O>): undefined;
23
23
  report(report: CheckerReport<T, O>): boolean;
24
- private _push;
25
24
  }
@@ -28,14 +28,14 @@ export class MLRuleContext {
28
28
  if (typeof report === 'function') {
29
29
  const r = report(this.translate);
30
30
  if (r) {
31
- this._push(r);
31
+ this.#push(r);
32
32
  return true;
33
33
  }
34
34
  return false;
35
35
  }
36
- this._push(report);
36
+ this.#push(report);
37
37
  }
38
- _push(report) {
38
+ #push(report) {
39
39
  const key = reportKey(report);
40
40
  if (!this.#reportKeys.has(key)) {
41
41
  this.#reportKeys.add(key);
@@ -83,7 +83,6 @@ export declare class MLRule<T extends RuleConfigValue, O extends PlainData = und
83
83
  * @returns An array of violations found by this rule
84
84
  */
85
85
  verify(document: MLDocument<T, O>, locale: LocaleSet, fix: boolean): Promise<Violation[]>;
86
- private _optimize;
87
86
  }
88
87
  /**
89
88
  * An MLRule with any value and option types. Used when the specific types are not known.
@@ -81,11 +81,11 @@ export class MLRule {
81
81
  * @returns The global rule info with node and child-node overrides
82
82
  */
83
83
  getRuleInfo(ruleSet, ruleName) {
84
- const info = this._optimize(ruleSet.rules, ruleName);
84
+ const info = this.#optimize(ruleSet.rules, ruleName);
85
85
  return {
86
86
  ...info,
87
- nodeRules: ruleSet.nodeRules.map(r => this._optimize(r.rules, ruleName)).filter(r => !r.disabled),
88
- childNodeRules: ruleSet.childNodeRules.map(r => this._optimize(r.rules, ruleName)).filter(r => !r.disabled),
87
+ nodeRules: ruleSet.nodeRules.map(r => this.#optimize(r.rules, ruleName)).filter(r => !r.disabled),
88
+ childNodeRules: ruleSet.childNodeRules.map(r => this.#optimize(r.rules, ruleName)).filter(r => !r.disabled),
89
89
  };
90
90
  }
91
91
  /**
@@ -196,7 +196,7 @@ export class MLRule {
196
196
  document.setRule(null);
197
197
  return violations;
198
198
  }
199
- _optimize(rules, ruleName) {
199
+ #optimize(rules, ruleName) {
200
200
  const rule = (rules?.[ruleName] ?? false);
201
201
  const info = this.optimizeOption(rule);
202
202
  return info;
@@ -1,25 +1,20 @@
1
- import type { IRuleFixer, TextEdit } from '@markuplint/ml-config';
1
+ import type { FixToken, IRuleFixer, TextEdit } from '@markuplint/ml-config';
2
2
  /**
3
3
  * Stateless implementation of {@link IRuleFixer}.
4
4
  * Provides helper methods for building {@link TextEdit} objects
5
5
  * inside rule fix callbacks.
6
6
  */
7
7
  export declare class RuleFixer implements IRuleFixer {
8
- replaceText(token: {
9
- readonly startOffset: number;
10
- readonly raw: string;
11
- }, text: string): TextEdit;
8
+ /** @see {@link IRuleFixer.replaceText} */
9
+ replaceText(token: FixToken, text: string): TextEdit;
10
+ /** @see {@link IRuleFixer.replaceRange} */
12
11
  replaceRange(range: readonly [number, number], text: string): TextEdit;
13
- insertBefore(token: {
14
- readonly startOffset: number;
15
- }, text: string): TextEdit;
16
- insertAfter(token: {
17
- readonly startOffset: number;
18
- readonly raw: string;
19
- }, text: string): TextEdit;
20
- remove(token: {
21
- readonly startOffset: number;
22
- readonly raw: string;
23
- }): TextEdit;
12
+ /** @see {@link IRuleFixer.insertBefore} */
13
+ insertBefore(token: Pick<FixToken, 'startOffset'>, text: string): TextEdit;
14
+ /** @see {@link IRuleFixer.insertAfter} */
15
+ insertAfter(token: FixToken, text: string): TextEdit;
16
+ /** @see {@link IRuleFixer.remove} */
17
+ remove(token: FixToken): TextEdit;
18
+ /** @see {@link IRuleFixer.removeRange} */
24
19
  removeRange(range: readonly [number, number]): TextEdit;
25
20
  }
@@ -4,28 +4,34 @@
4
4
  * inside rule fix callbacks.
5
5
  */
6
6
  export class RuleFixer {
7
+ /** @see {@link IRuleFixer.replaceText} */
7
8
  replaceText(token, text) {
8
9
  return {
9
10
  range: [token.startOffset, token.startOffset + token.raw.length],
10
11
  text,
11
12
  };
12
13
  }
14
+ /** @see {@link IRuleFixer.replaceRange} */
13
15
  replaceRange(range, text) {
14
16
  return { range, text };
15
17
  }
18
+ /** @see {@link IRuleFixer.insertBefore} */
16
19
  insertBefore(token, text) {
17
20
  return { range: [token.startOffset, token.startOffset], text };
18
21
  }
22
+ /** @see {@link IRuleFixer.insertAfter} */
19
23
  insertAfter(token, text) {
20
24
  const end = token.startOffset + token.raw.length;
21
25
  return { range: [end, end], text };
22
26
  }
27
+ /** @see {@link IRuleFixer.remove} */
23
28
  remove(token) {
24
29
  return {
25
30
  range: [token.startOffset, token.startOffset + token.raw.length],
26
31
  text: '',
27
32
  };
28
33
  }
34
+ /** @see {@link IRuleFixer.removeRange} */
29
35
  removeRange(range) {
30
36
  return { range, text: '' };
31
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-core",
3
- "version": "5.0.0-alpha.2",
3
+ "version": "5.0.0-dev.5+e96392f56",
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>",
@@ -30,20 +30,20 @@
30
30
  "./lib/configs.js": "./lib/configs.browser.js"
31
31
  },
32
32
  "dependencies": {
33
- "@markuplint/config-presets": "5.0.0-alpha.2",
34
- "@markuplint/html-parser": "5.0.0-alpha.2",
35
- "@markuplint/html-spec": "5.0.0-alpha.2",
36
- "@markuplint/i18n": "5.0.0-alpha.2",
37
- "@markuplint/ml-ast": "5.0.0-alpha.2",
38
- "@markuplint/ml-config": "5.0.0-alpha.2",
39
- "@markuplint/ml-spec": "5.0.0-alpha.2",
40
- "@markuplint/parser-utils": "5.0.0-alpha.2",
41
- "@markuplint/selector": "5.0.0-alpha.2",
42
- "@markuplint/shared": "5.0.0-alpha.2",
33
+ "@markuplint/config-presets": "5.0.0-dev.5+e96392f56",
34
+ "@markuplint/html-parser": "5.0.0-dev.5+e96392f56",
35
+ "@markuplint/html-spec": "5.0.0-dev.5+e96392f56",
36
+ "@markuplint/i18n": "5.0.0-dev.5+e96392f56",
37
+ "@markuplint/ml-ast": "5.0.0-dev.5+e96392f56",
38
+ "@markuplint/ml-config": "5.0.0-dev.5+e96392f56",
39
+ "@markuplint/ml-spec": "5.0.0-dev.5+e96392f56",
40
+ "@markuplint/parser-utils": "5.0.0-dev.5+e96392f56",
41
+ "@markuplint/selector": "5.0.0-dev.5+e96392f56",
42
+ "@markuplint/shared": "5.0.0-dev.5+e96392f56",
43
43
  "@types/debug": "4.1.12",
44
44
  "debug": "4.4.3",
45
45
  "is-plain-object": "5.0.0",
46
46
  "type-fest": "5.4.4"
47
47
  },
48
- "gitHead": "31ccf1e81443ea3f93597d287595211f1823ddcf"
48
+ "gitHead": "e96392f56e4bc8165ba59622b41c822703a96372"
49
49
  }