@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.
- package/ARCHITECTURE.ja.md +34 -1
- package/ARCHITECTURE.md +88 -5
- package/CHANGELOG.md +11 -0
- package/lib/cursor-offset.d.ts +13 -0
- package/lib/cursor-offset.js +34 -0
- package/lib/fix-applier.d.ts +3 -1
- package/lib/fix-applier.js +8 -3
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/ml-core.d.ts +41 -8
- package/lib/ml-core.js +187 -70
- package/lib/ml-dom/node/block.js +2 -1
- package/lib/ml-dom/node/character-data.d.ts +35 -0
- package/lib/ml-dom/node/character-data.js +35 -6
- package/lib/ml-dom/node/document.d.ts +0 -14
- package/lib/ml-dom/node/document.js +4 -5
- package/lib/ml-dom/node/dom-token-list.d.ts +0 -1
- package/lib/ml-dom/node/dom-token-list.js +3 -3
- package/lib/ml-dom/node/element.js +0 -3
- package/lib/ml-rule/ml-rule-context.d.ts +0 -1
- package/lib/ml-rule/ml-rule-context.js +3 -3
- package/lib/ml-rule/ml-rule.d.ts +0 -1
- package/lib/ml-rule/ml-rule.js +4 -4
- package/lib/ml-rule/rule-fixer.d.ts +11 -16
- package/lib/ml-rule/rule-fixer.js +6 -0
- package/package.json +12 -12
package/ARCHITECTURE.ja.md
CHANGED
|
@@ -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` パラメータは `
|
|
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
|
|
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
|
+
}
|
package/lib/fix-applier.d.ts
CHANGED
|
@@ -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.
|
package/lib/fix-applier.js
CHANGED
|
@@ -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
|
|
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():
|
|
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
|
|
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
|
-
|
|
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
|
|
74
|
-
this
|
|
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
|
|
90
|
-
this
|
|
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
|
|
132
|
+
this.#parse();
|
|
133
133
|
}
|
|
134
|
-
this
|
|
134
|
+
this.#createDocument();
|
|
135
135
|
}
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/ml-dom/node/block.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
110
|
+
this.#pretending(options?.pretenders);
|
|
111
111
|
try {
|
|
112
|
-
this
|
|
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
|
-
|
|
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
|
-
|
|
2300
|
+
#ruleMapping(ruleset) {
|
|
2302
2301
|
if (docLog.enabled) {
|
|
2303
2302
|
docLog('Rule Mapping: %O', Object.keys(ruleset.rules));
|
|
2304
2303
|
}
|
|
@@ -45,7 +45,7 @@ export class MLDomTokenList extends Array {
|
|
|
45
45
|
if (!token) {
|
|
46
46
|
break;
|
|
47
47
|
}
|
|
48
|
-
const loc = this
|
|
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
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -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
|
|
31
|
+
this.#push(r);
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
-
this
|
|
36
|
+
this.#push(report);
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
#push(report) {
|
|
39
39
|
const key = reportKey(report);
|
|
40
40
|
if (!this.#reportKeys.has(key)) {
|
|
41
41
|
this.#reportKeys.add(key);
|
package/lib/ml-rule/ml-rule.d.ts
CHANGED
|
@@ -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.
|
package/lib/ml-rule/ml-rule.js
CHANGED
|
@@ -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
|
|
84
|
+
const info = this.#optimize(ruleSet.rules, ruleName);
|
|
85
85
|
return {
|
|
86
86
|
...info,
|
|
87
|
-
nodeRules: ruleSet.nodeRules.map(r => this
|
|
88
|
-
childNodeRules: ruleSet.childNodeRules.map(r => this
|
|
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
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
insertAfter(token:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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-
|
|
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-
|
|
34
|
-
"@markuplint/html-parser": "5.0.0-
|
|
35
|
-
"@markuplint/html-spec": "5.0.0-
|
|
36
|
-
"@markuplint/i18n": "5.0.0-
|
|
37
|
-
"@markuplint/ml-ast": "5.0.0-
|
|
38
|
-
"@markuplint/ml-config": "5.0.0-
|
|
39
|
-
"@markuplint/ml-spec": "5.0.0-
|
|
40
|
-
"@markuplint/parser-utils": "5.0.0-
|
|
41
|
-
"@markuplint/selector": "5.0.0-
|
|
42
|
-
"@markuplint/shared": "5.0.0-
|
|
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": "
|
|
48
|
+
"gitHead": "e96392f56e4bc8165ba59622b41c822703a96372"
|
|
49
49
|
}
|