mdrefine 0.2.0-rc.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/CONTRIBUTING.md +2 -1
- package/README.md +1 -1
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +247 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/main.js +14 -7
- package/dist/cli/main.js.map +1 -1
- package/dist/rules/blocks.js +17 -5
- package/dist/rules/blocks.js.map +1 -1
- package/dist/rules/style.js +93 -21
- package/dist/rules/style.js.map +1 -1
- package/dist/syntax/parse.d.ts +14 -1
- package/dist/syntax/parse.js +25 -3
- package/dist/syntax/parse.js.map +1 -1
- package/dist/workspace/discovery.d.ts +8 -3
- package/dist/workspace/discovery.js +21 -9
- package/dist/workspace/discovery.js.map +1 -1
- package/dist/workspace/files.d.ts +17 -1
- package/dist/workspace/files.js +82 -6
- package/dist/workspace/files.js.map +1 -1
- package/docs/api.md +3 -1
- package/docs/benchmarks.md +41 -41
- package/docs/cli.md +17 -7
- package/docs/design.md +5 -0
- package/docs/dialects.md +2 -1
- package/docs/integrations.md +8 -2
- package/docs/quick-start.md +1 -1
- package/docs/rules.md +18 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,90 @@ release's section is also its [GitHub
|
|
|
8
8
|
release](https://github.com/viell-dev/slop-markdown-tools/releases)'s notes; the
|
|
9
9
|
prereleases' notes there are longer than their entries here.
|
|
10
10
|
|
|
11
|
+
## 0.2.1 - 2026-10-18
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- The Obsidian dialect reads strikethrough with two tildes only, as Obsidian
|
|
16
|
+
shows it; `~a~` is text there. Before, `style/emphasis` left emphasis that the
|
|
17
|
+
misread strikethrough covered unchanged
|
|
18
|
+
([#327](https://github.com/viell-dev/slop-markdown-tools/pull/327)).
|
|
19
|
+
- `style/emphasis` and `style/strong` no longer change markers that GitHub reads
|
|
20
|
+
otherwise than the parser: after an earlier emphasis, in `~~b *c~~ d*`, where
|
|
21
|
+
GitHub shows the asterisks as text; next to tildes, in `x~~*b b*~~`, where a
|
|
22
|
+
`_` would be text on GitHub; and in runs of markers that the parser pairs
|
|
23
|
+
otherwise than CommonMark, as in `xab***a* a*y`. Each rewrite changed what
|
|
24
|
+
GitHub showed while the safety check, which uses the parser's reading, saw no
|
|
25
|
+
change ([#327](https://github.com/viell-dev/slop-markdown-tools/pull/327)).
|
|
26
|
+
|
|
27
|
+
## 0.2.0 - 2026-10-18
|
|
28
|
+
|
|
29
|
+
The first stable release. It holds everything in the earlier prereleases, and
|
|
30
|
+
the changes since `0.2.0-rc.3` listed here. From this release on, npm's `latest`
|
|
31
|
+
tag names the newest stable release, and prereleases stay under `rc` and `beta`;
|
|
32
|
+
the [versioning
|
|
33
|
+
policy](https://viell-dev.github.io/slop-markdown-tools/versions.html) says what
|
|
34
|
+
a version number promises.
|
|
35
|
+
|
|
36
|
+
### Upgrading from a prerelease
|
|
37
|
+
|
|
38
|
+
From `0.2.0-rc.3`:
|
|
39
|
+
|
|
40
|
+
- In the Obsidian dialect, `format` no longer breaks lines inside the text of a
|
|
41
|
+
link, and joins link text that is written across lines, also where
|
|
42
|
+
`0.2.0-rc.3` broke it. Notes that `0.2.0-rc.3` formatted change again: run
|
|
43
|
+
`format --write` once after upgrading.
|
|
44
|
+
- A document from stdin whose `--stdin-filepath` is ignored is no longer
|
|
45
|
+
processed: `lint -` reports nothing and `format -` prints it unchanged, with a
|
|
46
|
+
warning. In `mdrefine/node`, `discoverWorkspace` no longer selects `documents`
|
|
47
|
+
whose paths are ignored but lists them in `skipped`.
|
|
48
|
+
|
|
49
|
+
From an earlier prerelease, also read the upgrade notes of
|
|
50
|
+
[0.2.0-rc.3](https://github.com/viell-dev/slop-markdown-tools/releases/tag/0.2.0-rc.3).
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- `--no-warn-ignored` leaves out the warning for a file or directory named on
|
|
55
|
+
the command line that the tool skips as ignored, for hooks and scripts that
|
|
56
|
+
pass every changed file. The JSON report still lists it in `skipped`
|
|
57
|
+
([#311](https://github.com/viell-dev/slop-markdown-tools/pull/311)).
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- `format --diff`, which `format` also does for files by default, takes about as
|
|
62
|
+
long as `format --check` when formatting changes lines throughout a long
|
|
63
|
+
document. Before, building the diff took time in proportion to the document's
|
|
64
|
+
length times its changed lines: two minutes for a generated document of 8,000
|
|
65
|
+
wrapped paragraphs that took four seconds to check
|
|
66
|
+
([#315](https://github.com/viell-dev/slop-markdown-tools/pull/315)).
|
|
67
|
+
- A document from stdin whose `--stdin-filepath` the configuration, Git, or
|
|
68
|
+
discovery ignores is left out as a named file is: `lint -` reports nothing,
|
|
69
|
+
`format -` prints the document unchanged, and a warning says why. Before, it
|
|
70
|
+
was linted and formatted, so an editor's format-on-save rewrote a document
|
|
71
|
+
that the configuration says to leave alone. In the library, `documents` that
|
|
72
|
+
ignore rules leave out are no longer selected
|
|
73
|
+
([#316](https://github.com/viell-dev/slop-markdown-tools/pull/316)).
|
|
74
|
+
- Stdin in a workspace root that Git ignores no longer warns that the root's
|
|
75
|
+
files are not processed
|
|
76
|
+
([#316](https://github.com/viell-dev/slop-markdown-tools/pull/316)).
|
|
77
|
+
- In the Obsidian dialect, `style/wrap` keeps the text of a link on one line and
|
|
78
|
+
moves the link whole to the next line where it does not fit, and joins link
|
|
79
|
+
text written across lines. Obsidian's Live Preview shows a link whose text
|
|
80
|
+
spans lines as Markdown source. `0.2.0-rc.3` broke lines inside link text in
|
|
81
|
+
every dialect; elsewhere it still does, as GitHub, Forgejo, and Gitea render
|
|
82
|
+
such links
|
|
83
|
+
([#312](https://github.com/viell-dev/slop-markdown-tools/pull/312)).
|
|
84
|
+
- Lists and block quotes below a first line of `---` or `+++` that no later line
|
|
85
|
+
closes are read as lists and block quotes, as renderers show them. Before, the
|
|
86
|
+
parser read them as paragraphs, and `style/wrap` joined them to the line
|
|
87
|
+
above, which GitHub then showed as text
|
|
88
|
+
([#309](https://github.com/viell-dev/slop-markdown-tools/pull/309)).
|
|
89
|
+
- `style/thematic-break` and `style/heading` with `setext` no longer write a
|
|
90
|
+
line of `---` alone below such a first line of `---`, which made front matter
|
|
91
|
+
of everything between the two lines. The safety check refused every edit of
|
|
92
|
+
the rule in such a document
|
|
93
|
+
([#309](https://github.com/viell-dev/slop-markdown-tools/pull/309)).
|
|
94
|
+
|
|
11
95
|
## 0.2.0-rc.3 - 2026-10-18
|
|
12
96
|
|
|
13
97
|
### Upgrading from a prerelease
|
package/CONTRIBUTING.md
CHANGED
|
@@ -106,7 +106,8 @@ thematic breaks, and blank space in `blocks.test.ts`, problem rules in
|
|
|
106
106
|
`references.test.ts`, `structure.test.ts`, and `content.test.ts`, wrapping in
|
|
107
107
|
`wrap.test.ts` and `callout-wrap.test.ts`, links in `links.test.ts`, dollar math
|
|
108
108
|
in `math.test.ts` against the renderers' output recorded in `math-renderers.ts`,
|
|
109
|
-
|
|
109
|
+
the engine, configuration, and plugins in `engine.test.ts`, and the diff that
|
|
110
|
+
`format` prints in `diff.test.ts`.
|
|
110
111
|
|
|
111
112
|
`tests/integration/` runs the built CLI as a child process in temporary
|
|
112
113
|
workspaces, and runs `examples/` and `benchmarks/` as they are, without the
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ mdrefine needs Node.js 22.12 or newer. The package is `mdrefine`, and its
|
|
|
59
59
|
command is `mdtools`:
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
|
-
npm install --save-dev --save-exact mdrefine@0.2.
|
|
62
|
+
npm install --save-dev --save-exact mdrefine@0.2.1
|
|
63
63
|
npx mdtools lint
|
|
64
64
|
npx mdtools format --diff
|
|
65
65
|
npx mdtools format --write
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unified diff that turns `before` into `after`, with the headers and hunks
|
|
3
|
+
* of `diff`'s `createTwoFilesPatch`. That function compares the whole texts,
|
|
4
|
+
* at a cost that grows with their length times the number of changed lines, so
|
|
5
|
+
* that rewrapping a long document took minutes to show. This one compares only
|
|
6
|
+
* the regions that changed and their neighbours. Where it removes and adds
|
|
7
|
+
* other lines than a comparison of the whole texts would, both diffs turn
|
|
8
|
+
* `before` into `after`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function unifiedDiff(oldFileName: string, newFileName: string, before: string, after: string): string;
|
package/dist/cli/diff.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { diffArrays, formatPatch } from "diff";
|
|
2
|
+
/** Lines of context around each change, as `diff`'s `createTwoFilesPatch` shows them. */
|
|
3
|
+
const context = 4;
|
|
4
|
+
/**
|
|
5
|
+
* The most lines, of both texts together, that one run of the line diff
|
|
6
|
+
* compares. Its cost grows with the lines it compares times the lines that
|
|
7
|
+
* changed, so this limit keeps the cost of the whole diff in proportion to the
|
|
8
|
+
* length of the texts.
|
|
9
|
+
*/
|
|
10
|
+
const limit = 100;
|
|
11
|
+
/** The lines of `text`, each with its line break, split where `diff` splits them. */
|
|
12
|
+
function lines(text) {
|
|
13
|
+
const result = [];
|
|
14
|
+
for (let start = 0; start < text.length;) {
|
|
15
|
+
const end = text.indexOf("\n", start) + 1 || text.length;
|
|
16
|
+
result.push(text.slice(start, end));
|
|
17
|
+
start = end;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The longest chain of `pairs`, which are in increasing order of their first
|
|
23
|
+
* item, whose second items increase too.
|
|
24
|
+
*/
|
|
25
|
+
function chain(pairs) {
|
|
26
|
+
// The pair that ends the longest chain of each length so far, and each pair's predecessor.
|
|
27
|
+
const ends = [];
|
|
28
|
+
const previous = [];
|
|
29
|
+
pairs.forEach(([, place], index) => {
|
|
30
|
+
let low = 0;
|
|
31
|
+
for (let high = ends.length; low < high;) {
|
|
32
|
+
const middle = (low + high) >> 1;
|
|
33
|
+
if (pairs[ends[middle]][1] < place)
|
|
34
|
+
low = middle + 1;
|
|
35
|
+
else
|
|
36
|
+
high = middle;
|
|
37
|
+
}
|
|
38
|
+
previous[index] = low > 0 ? ends[low - 1] : -1;
|
|
39
|
+
ends[low] = index;
|
|
40
|
+
});
|
|
41
|
+
const result = [];
|
|
42
|
+
for (let index = ends.at(-1) ?? -1; index >= 0; index = previous[index])
|
|
43
|
+
result.push(pairs[index]);
|
|
44
|
+
return result.reverse();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The regions where `after` differs from `before`, in order, each followed by
|
|
48
|
+
* a line that both texts share, unless it reaches their ends.
|
|
49
|
+
*
|
|
50
|
+
* Lines that occur once in each text, in the same order in both, are anchors
|
|
51
|
+
* that the texts keep, as in patience diff, so that a region never reaches past
|
|
52
|
+
* an unchanged paragraph or heading. Between anchors, at a difference, the
|
|
53
|
+
* texts agree again at the nearest pair of equal lines: the one that skips the
|
|
54
|
+
* fewest lines of both texts together. An index of where each line occurs in
|
|
55
|
+
* `after` finds that pair in time proportional to the lines it skips, so the
|
|
56
|
+
* scan takes time in proportion to the length of the texts, however much
|
|
57
|
+
* changed. No line of a region equals a line of the other text's part of it,
|
|
58
|
+
* since that pair would have been nearer.
|
|
59
|
+
*/
|
|
60
|
+
function regions(before, after) {
|
|
61
|
+
const occurrences = new Map();
|
|
62
|
+
after.forEach((line, index) => {
|
|
63
|
+
const entry = occurrences.get(line);
|
|
64
|
+
if (entry)
|
|
65
|
+
entry.places.push(index);
|
|
66
|
+
else
|
|
67
|
+
occurrences.set(line, { places: [index], passed: 0, old: 0 });
|
|
68
|
+
});
|
|
69
|
+
for (const line of before) {
|
|
70
|
+
const entry = occurrences.get(line);
|
|
71
|
+
if (entry)
|
|
72
|
+
entry.old++;
|
|
73
|
+
}
|
|
74
|
+
const anchors = chain(before.flatMap((line, index) => {
|
|
75
|
+
const entry = occurrences.get(line);
|
|
76
|
+
return entry?.old === 1 && entry.places.length === 1 ? [[index, entry.places[0]]] : [];
|
|
77
|
+
}));
|
|
78
|
+
/** The first place of `line` in `after` at or after `from`, which never decreases. */
|
|
79
|
+
const next = (line, from) => {
|
|
80
|
+
const entry = occurrences.get(line);
|
|
81
|
+
if (!entry)
|
|
82
|
+
return undefined;
|
|
83
|
+
while ((entry.places[entry.passed] ?? Infinity) < from)
|
|
84
|
+
entry.passed++;
|
|
85
|
+
return entry.places[entry.passed];
|
|
86
|
+
};
|
|
87
|
+
const found = [];
|
|
88
|
+
let anchor = 0;
|
|
89
|
+
for (let oldLine = 0, newLine = 0; oldLine < before.length || newLine < after.length;) {
|
|
90
|
+
if (before[oldLine] === after[newLine]) {
|
|
91
|
+
oldLine++;
|
|
92
|
+
newLine++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
while ((anchors[anchor]?.[0] ?? Infinity) < oldLine)
|
|
96
|
+
anchor++;
|
|
97
|
+
// The region ends at the next anchor, or at the ends of the texts, unless a pair is nearer.
|
|
98
|
+
const [oldLimit, newLimit] = anchors[anchor] ?? [before.length, after.length];
|
|
99
|
+
let oldEnd = oldLimit;
|
|
100
|
+
let newEnd = newLimit;
|
|
101
|
+
let skipped = oldEnd - oldLine + newEnd - newLine;
|
|
102
|
+
for (let line = oldLine; line < oldLimit && line - oldLine < skipped; line++) {
|
|
103
|
+
const place = next(before[line], newLine);
|
|
104
|
+
if (place !== undefined && place < newLimit && line - oldLine + place - newLine < skipped) {
|
|
105
|
+
skipped = line - oldLine + place - newLine;
|
|
106
|
+
oldEnd = line;
|
|
107
|
+
newEnd = place;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
found.push({ oldStart: oldLine, oldEnd, newStart: newLine, newEnd });
|
|
111
|
+
oldLine = oldEnd;
|
|
112
|
+
newLine = newEnd;
|
|
113
|
+
}
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The runs of lines that turn `before` into `after`. The line diff, which
|
|
118
|
+
* finds the fewest lines to remove and add, compares each group of nearby
|
|
119
|
+
* regions that fits within the limit, so that a pair of equal lines that only
|
|
120
|
+
* happens to be the nearest, such as two blank lines, does not decide how a
|
|
121
|
+
* change is shown. A group's lines run on to the next group, or to the ends,
|
|
122
|
+
* so that texts with one group get the diff of the whole texts. A region
|
|
123
|
+
* larger than the limit has no line in common with the other text's part of
|
|
124
|
+
* it, so the line diff would remove and add it whole, as this does.
|
|
125
|
+
*/
|
|
126
|
+
function runs(before, after) {
|
|
127
|
+
const result = [];
|
|
128
|
+
const add = (kind, count) => {
|
|
129
|
+
const last = result.at(-1);
|
|
130
|
+
if (count === 0)
|
|
131
|
+
return;
|
|
132
|
+
if (last?.kind === kind)
|
|
133
|
+
last.count += count;
|
|
134
|
+
else
|
|
135
|
+
result.push({ kind, count });
|
|
136
|
+
};
|
|
137
|
+
const found = regions(before, after);
|
|
138
|
+
add(" ", found[0]?.oldStart ?? before.length);
|
|
139
|
+
for (let index = 0; index < found.length;) {
|
|
140
|
+
const first = found[index];
|
|
141
|
+
const size = (region) => region.oldEnd - first.oldStart + region.newEnd - first.newStart;
|
|
142
|
+
let end = index + 1;
|
|
143
|
+
while (end < found.length && size(found[end]) <= limit)
|
|
144
|
+
end++;
|
|
145
|
+
const last = found[end - 1];
|
|
146
|
+
const oldEnd = found[end]?.oldStart ?? before.length;
|
|
147
|
+
const newEnd = found[end]?.newStart ?? after.length;
|
|
148
|
+
if (size(last) > limit) {
|
|
149
|
+
add("-", last.oldEnd - first.oldStart);
|
|
150
|
+
add("+", last.newEnd - first.newStart);
|
|
151
|
+
add(" ", oldEnd - last.oldEnd);
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
for (const change of diffArrays(before.slice(first.oldStart, oldEnd), after.slice(first.newStart, newEnd)))
|
|
155
|
+
add(change.added ? "+" : change.removed ? "-" : " ", change.count);
|
|
156
|
+
index = end;
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The hunks of `changes`, as `diff`'s `structuredPatch` makes them: up to
|
|
162
|
+
* `context` kept lines on each side of a change, and one hunk for changes
|
|
163
|
+
* whose contexts touch.
|
|
164
|
+
*/
|
|
165
|
+
function hunks(before, after, changes) {
|
|
166
|
+
const result = [];
|
|
167
|
+
let hunk;
|
|
168
|
+
/** Adds a line to the hunk as a patch shows it, and marks a last line without a line break. */
|
|
169
|
+
const show = (prefix, line) => {
|
|
170
|
+
if (line.endsWith("\n"))
|
|
171
|
+
hunk.lines.push(prefix + line.slice(0, -1));
|
|
172
|
+
else
|
|
173
|
+
hunk.lines.push(prefix + line, "\");
|
|
174
|
+
if (prefix !== "+")
|
|
175
|
+
hunk.oldLines++;
|
|
176
|
+
if (prefix !== "-")
|
|
177
|
+
hunk.newLines++;
|
|
178
|
+
};
|
|
179
|
+
let oldLine = 0;
|
|
180
|
+
let newLine = 0;
|
|
181
|
+
changes.forEach((run, index) => {
|
|
182
|
+
if (run.kind === " ") {
|
|
183
|
+
if (hunk) {
|
|
184
|
+
// When another change follows within twice the context, the hunk takes it in.
|
|
185
|
+
const shown = run.count <= 2 * context && index < changes.length - 1
|
|
186
|
+
? run.count
|
|
187
|
+
: Math.min(run.count, context);
|
|
188
|
+
for (const line of before.slice(oldLine, oldLine + shown))
|
|
189
|
+
show(" ", line);
|
|
190
|
+
if (shown < run.count || index === changes.length - 1) {
|
|
191
|
+
result.push(hunk);
|
|
192
|
+
hunk = undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
oldLine += run.count;
|
|
196
|
+
newLine += run.count;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!hunk) {
|
|
200
|
+
// The kept lines before the change, which the previous run holds.
|
|
201
|
+
const leading = Math.min(changes[index - 1]?.count ?? 0, context);
|
|
202
|
+
hunk = {
|
|
203
|
+
oldStart: oldLine - leading + 1,
|
|
204
|
+
oldLines: 0,
|
|
205
|
+
newStart: newLine - leading + 1,
|
|
206
|
+
newLines: 0,
|
|
207
|
+
lines: [],
|
|
208
|
+
};
|
|
209
|
+
for (const line of before.slice(oldLine - leading, oldLine))
|
|
210
|
+
show(" ", line);
|
|
211
|
+
}
|
|
212
|
+
if (run.kind === "-") {
|
|
213
|
+
for (const line of before.slice(oldLine, oldLine + run.count))
|
|
214
|
+
show("-", line);
|
|
215
|
+
oldLine += run.count;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
for (const line of after.slice(newLine, newLine + run.count))
|
|
219
|
+
show("+", line);
|
|
220
|
+
newLine += run.count;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
if (hunk)
|
|
224
|
+
result.push(hunk);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* A unified diff that turns `before` into `after`, with the headers and hunks
|
|
229
|
+
* of `diff`'s `createTwoFilesPatch`. That function compares the whole texts,
|
|
230
|
+
* at a cost that grows with their length times the number of changed lines, so
|
|
231
|
+
* that rewrapping a long document took minutes to show. This one compares only
|
|
232
|
+
* the regions that changed and their neighbours. Where it removes and adds
|
|
233
|
+
* other lines than a comparison of the whole texts would, both diffs turn
|
|
234
|
+
* `before` into `after`.
|
|
235
|
+
*/
|
|
236
|
+
export function unifiedDiff(oldFileName, newFileName, before, after) {
|
|
237
|
+
const oldLines = lines(before);
|
|
238
|
+
const newLines = lines(after);
|
|
239
|
+
return formatPatch({
|
|
240
|
+
oldFileName,
|
|
241
|
+
newFileName,
|
|
242
|
+
oldHeader: undefined,
|
|
243
|
+
newHeader: undefined,
|
|
244
|
+
hunks: hunks(oldLines, newLines, runs(oldLines, newLines)),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/cli/diff.ts"],"names":[],"mappings":"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","sourcesContent":["import { diffArrays, formatPatch, type StructuredPatchHunk } from \"diff\";\n\n/** Lines of context around each change, as `diff`'s `createTwoFilesPatch` shows them. */\nconst context = 4;\n/**\n * The most lines, of both texts together, that one run of the line diff\n * compares. Its cost grows with the lines it compares times the lines that\n * changed, so this limit keeps the cost of the whole diff in proportion to the\n * length of the texts.\n */\nconst limit = 100;\n\n/** The lines of `text`, each with its line break, split where `diff` splits them. */\nfunction lines(text: string): string[] {\n const result: string[] = [];\n for (let start = 0; start < text.length;) {\n const end = text.indexOf(\"\\n\", start) + 1 || text.length;\n result.push(text.slice(start, end));\n start = end;\n }\n return result;\n}\n\n/** Lines `oldStart` to `oldEnd` of the old text, which became `newStart` to `newEnd` of the new. */\ninterface Region {\n oldStart: number;\n oldEnd: number;\n newStart: number;\n newEnd: number;\n}\n\n/**\n * Where a line occurs: its places in the new text, how many of them the scan\n * has passed, and how often it occurs in the old text.\n */\ninterface Occurrences {\n places: number[];\n passed: number;\n old: number;\n}\n\n/**\n * The longest chain of `pairs`, which are in increasing order of their first\n * item, whose second items increase too.\n */\nfunction chain(pairs: [number, number][]): [number, number][] {\n // The pair that ends the longest chain of each length so far, and each pair's predecessor.\n const ends: number[] = [];\n const previous: number[] = [];\n pairs.forEach(([, place], index) => {\n let low = 0;\n for (let high = ends.length; low < high;) {\n const middle = (low + high) >> 1;\n if (pairs[ends[middle]!]![1] < place) low = middle + 1;\n else high = middle;\n }\n previous[index] = low > 0 ? ends[low - 1]! : -1;\n ends[low] = index;\n });\n const result: [number, number][] = [];\n for (let index = ends.at(-1) ?? -1; index >= 0; index = previous[index]!)\n result.push(pairs[index]!);\n return result.reverse();\n}\n\n/**\n * The regions where `after` differs from `before`, in order, each followed by\n * a line that both texts share, unless it reaches their ends.\n *\n * Lines that occur once in each text, in the same order in both, are anchors\n * that the texts keep, as in patience diff, so that a region never reaches past\n * an unchanged paragraph or heading. Between anchors, at a difference, the\n * texts agree again at the nearest pair of equal lines: the one that skips the\n * fewest lines of both texts together. An index of where each line occurs in\n * `after` finds that pair in time proportional to the lines it skips, so the\n * scan takes time in proportion to the length of the texts, however much\n * changed. No line of a region equals a line of the other text's part of it,\n * since that pair would have been nearer.\n */\nfunction regions(before: string[], after: string[]): Region[] {\n const occurrences = new Map<string, Occurrences>();\n after.forEach((line, index) => {\n const entry = occurrences.get(line);\n if (entry) entry.places.push(index);\n else occurrences.set(line, { places: [index], passed: 0, old: 0 });\n });\n for (const line of before) {\n const entry = occurrences.get(line);\n if (entry) entry.old++;\n }\n const anchors = chain(\n before.flatMap((line, index): [number, number][] => {\n const entry = occurrences.get(line);\n return entry?.old === 1 && entry.places.length === 1 ? [[index, entry.places[0]!]] : [];\n }),\n );\n /** The first place of `line` in `after` at or after `from`, which never decreases. */\n const next = (line: string, from: number) => {\n const entry = occurrences.get(line);\n if (!entry) return undefined;\n while ((entry.places[entry.passed] ?? Infinity) < from) entry.passed++;\n return entry.places[entry.passed];\n };\n const found: Region[] = [];\n let anchor = 0;\n for (let oldLine = 0, newLine = 0; oldLine < before.length || newLine < after.length;) {\n if (before[oldLine] === after[newLine]) {\n oldLine++;\n newLine++;\n continue;\n }\n while ((anchors[anchor]?.[0] ?? Infinity) < oldLine) anchor++;\n // The region ends at the next anchor, or at the ends of the texts, unless a pair is nearer.\n const [oldLimit, newLimit] = anchors[anchor] ?? [before.length, after.length];\n let oldEnd = oldLimit;\n let newEnd = newLimit;\n let skipped = oldEnd - oldLine + newEnd - newLine;\n for (let line = oldLine; line < oldLimit && line - oldLine < skipped; line++) {\n const place = next(before[line]!, newLine);\n if (place !== undefined && place < newLimit && line - oldLine + place - newLine < skipped) {\n skipped = line - oldLine + place - newLine;\n oldEnd = line;\n newEnd = place;\n }\n }\n found.push({ oldStart: oldLine, oldEnd, newStart: newLine, newEnd });\n oldLine = oldEnd;\n newLine = newEnd;\n }\n return found;\n}\n\n/** Lines that both texts keep (\" \"), that the old text loses (\"-\"), or that the new gains (\"+\"). */\ninterface Run {\n kind: \" \" | \"-\" | \"+\";\n count: number;\n}\n\n/**\n * The runs of lines that turn `before` into `after`. The line diff, which\n * finds the fewest lines to remove and add, compares each group of nearby\n * regions that fits within the limit, so that a pair of equal lines that only\n * happens to be the nearest, such as two blank lines, does not decide how a\n * change is shown. A group's lines run on to the next group, or to the ends,\n * so that texts with one group get the diff of the whole texts. A region\n * larger than the limit has no line in common with the other text's part of\n * it, so the line diff would remove and add it whole, as this does.\n */\nfunction runs(before: string[], after: string[]): Run[] {\n const result: Run[] = [];\n const add = (kind: Run[\"kind\"], count: number) => {\n const last = result.at(-1);\n if (count === 0) return;\n if (last?.kind === kind) last.count += count;\n else result.push({ kind, count });\n };\n const found = regions(before, after);\n add(\" \", found[0]?.oldStart ?? before.length);\n for (let index = 0; index < found.length;) {\n const first = found[index]!;\n const size = (region: Region) =>\n region.oldEnd - first.oldStart + region.newEnd - first.newStart;\n let end = index + 1;\n while (end < found.length && size(found[end]!) <= limit) end++;\n const last = found[end - 1]!;\n const oldEnd = found[end]?.oldStart ?? before.length;\n const newEnd = found[end]?.newStart ?? after.length;\n if (size(last) > limit) {\n add(\"-\", last.oldEnd - first.oldStart);\n add(\"+\", last.newEnd - first.newStart);\n add(\" \", oldEnd - last.oldEnd);\n } else\n for (const change of diffArrays(\n before.slice(first.oldStart, oldEnd),\n after.slice(first.newStart, newEnd),\n ))\n add(change.added ? \"+\" : change.removed ? \"-\" : \" \", change.count);\n index = end;\n }\n return result;\n}\n\n/**\n * The hunks of `changes`, as `diff`'s `structuredPatch` makes them: up to\n * `context` kept lines on each side of a change, and one hunk for changes\n * whose contexts touch.\n */\nfunction hunks(before: string[], after: string[], changes: Run[]): StructuredPatchHunk[] {\n const result: StructuredPatchHunk[] = [];\n let hunk: StructuredPatchHunk | undefined;\n /** Adds a line to the hunk as a patch shows it, and marks a last line without a line break. */\n const show = (prefix: Run[\"kind\"], line: string) => {\n if (line.endsWith(\"\\n\")) hunk!.lines.push(prefix + line.slice(0, -1));\n else hunk!.lines.push(prefix + line, \"\\\\");\n if (prefix !== \"+\") hunk!.oldLines++;\n if (prefix !== \"-\") hunk!.newLines++;\n };\n let oldLine = 0;\n let newLine = 0;\n changes.forEach((run, index) => {\n if (run.kind === \" \") {\n if (hunk) {\n // When another change follows within twice the context, the hunk takes it in.\n const shown =\n run.count <= 2 * context && index < changes.length - 1\n ? run.count\n : Math.min(run.count, context);\n for (const line of before.slice(oldLine, oldLine + shown)) show(\" \", line);\n if (shown < run.count || index === changes.length - 1) {\n result.push(hunk);\n hunk = undefined;\n }\n }\n oldLine += run.count;\n newLine += run.count;\n return;\n }\n if (!hunk) {\n // The kept lines before the change, which the previous run holds.\n const leading = Math.min(changes[index - 1]?.count ?? 0, context);\n hunk = {\n oldStart: oldLine - leading + 1,\n oldLines: 0,\n newStart: newLine - leading + 1,\n newLines: 0,\n lines: [],\n };\n for (const line of before.slice(oldLine - leading, oldLine)) show(\" \", line);\n }\n if (run.kind === \"-\") {\n for (const line of before.slice(oldLine, oldLine + run.count)) show(\"-\", line);\n oldLine += run.count;\n } else {\n for (const line of after.slice(newLine, newLine + run.count)) show(\"+\", line);\n newLine += run.count;\n }\n });\n if (hunk) result.push(hunk);\n return result;\n}\n\n/**\n * A unified diff that turns `before` into `after`, with the headers and hunks\n * of `diff`'s `createTwoFilesPatch`. That function compares the whole texts,\n * at a cost that grows with their length times the number of changed lines, so\n * that rewrapping a long document took minutes to show. This one compares only\n * the regions that changed and their neighbours. Where it removes and adds\n * other lines than a comparison of the whole texts would, both diffs turn\n * `before` into `after`.\n */\nexport function unifiedDiff(\n oldFileName: string,\n newFileName: string,\n before: string,\n after: string,\n): string {\n const oldLines = lines(before);\n const newLines = lines(after);\n return formatPatch({\n oldFileName,\n newFileName,\n oldHeader: undefined,\n newHeader: undefined,\n hunks: hunks(oldLines, newLines, runs(oldLines, newLines)),\n });\n}\n"]}
|
package/dist/cli/main.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
|
|
3
|
-
import { createTwoFilesPatch } from "diff";
|
|
4
3
|
import { readFile, realpath } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { format, lint, ruleRegistry } from "../core/engine.js";
|
|
8
7
|
import { loadConfig } from "../config/load.js";
|
|
9
8
|
import { dialectAliases, dialects, resolveConfig } from "../config/resolve.js";
|
|
10
|
-
import { replacementFailure, replacementProblem, skippedMessage, writeFormatted, } from "../workspace/files.js";
|
|
9
|
+
import { isIgnoredInput, replacementFailure, replacementProblem, skippedMessage, writeFormatted, } from "../workspace/files.js";
|
|
11
10
|
import { assumedDialect, checkRoot, discoverFrom } from "../workspace/discovery.js";
|
|
12
11
|
import { enclosingVault } from "../workspace/vault.js";
|
|
13
12
|
import { refusal, refusalText } from "../workspace/access.js";
|
|
14
13
|
import { decodeText, NotUtf8Error } from "../workspace/encoding.js";
|
|
14
|
+
import { unifiedDiff } from "./diff.js";
|
|
15
15
|
const manifest = JSON.parse(await readFile(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf8"));
|
|
16
16
|
// Subcommands inherit these settings, so they come before the commands.
|
|
17
17
|
const program = new Command()
|
|
@@ -53,6 +53,7 @@ function common(command) {
|
|
|
53
53
|
.option("--exclude <path>", "Exclude an exact file or directory (repeatable)", (value, previous = []) => [...previous, value])
|
|
54
54
|
.option("--json", "Machine-readable output")
|
|
55
55
|
.option("--quiet", "Report errors only: leave warnings out of the report and its counts")
|
|
56
|
+
.option("--no-warn-ignored", "Do not warn about named files and directories that are skipped as ignored")
|
|
56
57
|
.option("--stdin-filepath <file>", "Filename context for stdin")
|
|
57
58
|
.option("--max-warnings <count>", "Fail when warning count exceeds this nonnegative integer", (value) => {
|
|
58
59
|
const number = Number(value);
|
|
@@ -148,7 +149,9 @@ async function run(mode, inputs, flags) {
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
// Paths on the command line are relative to the working directory, and no
|
|
151
|
-
// path selects the whole workspace; stdin is the only document then.
|
|
152
|
+
// path selects the whole workspace; stdin is the only document then. Ignore
|
|
153
|
+
// rules apply to the path that `--stdin-filepath` names, as to a named file,
|
|
154
|
+
// and not to the default name.
|
|
152
155
|
const discovery = await discoverFrom({
|
|
153
156
|
root,
|
|
154
157
|
...(stdin ? { inputs: [] } : inputs.length ? { inputs } : {}),
|
|
@@ -156,7 +159,7 @@ async function run(mode, inputs, flags) {
|
|
|
156
159
|
config: loaded.config,
|
|
157
160
|
plugins: loaded.plugins,
|
|
158
161
|
...(documents ? { documents } : {}),
|
|
159
|
-
}, process.cwd());
|
|
162
|
+
}, process.cwd(), flags.stdinFilepath !== undefined);
|
|
160
163
|
const reports = [];
|
|
161
164
|
const changes = [];
|
|
162
165
|
// Selected files that the system refused to let the tool read, or that are not UTF-8 text.
|
|
@@ -274,13 +277,17 @@ async function run(mode, inputs, flags) {
|
|
|
274
277
|
const here = await realpath(process.cwd()).catch(() => process.cwd());
|
|
275
278
|
const shown = (name) => path.relative(here, path.join(discovery.root, name)).split(path.sep).join("/");
|
|
276
279
|
// Warnings about the run, not about a document: they do not change the exit status.
|
|
280
|
+
// A caller that passes every changed file can leave out those about ignored ones.
|
|
277
281
|
for (const item of discovery.skipped)
|
|
278
|
-
|
|
282
|
+
if (flags.warnIgnored !== false || !isIgnoredInput(item))
|
|
283
|
+
process.stderr.write(`mdtools: warning: ${skippedMessage(item, shown)}\n`);
|
|
284
|
+
// An ignored document from stdin comes back unchanged, as in Prettier, so
|
|
285
|
+
// that an editor that replaces its text with the output loses nothing.
|
|
279
286
|
if (stdin && mode === "format" && !flags.check && !flags.diff)
|
|
280
|
-
process.stdout.write(reports[0]?.output ?? "");
|
|
287
|
+
process.stdout.write(reports[0]?.output ?? Object.values(documents ?? {})[0] ?? "");
|
|
281
288
|
else if (mode === "format" && !flags.check && !flags.write)
|
|
282
289
|
for (const change of changes)
|
|
283
|
-
process.stdout.write(
|
|
290
|
+
process.stdout.write(unifiedDiff(`a/${shown(change.file)}`, `b/${shown(change.file)}`, change.before, change.after));
|
|
284
291
|
for (const report of reports)
|
|
285
292
|
for (const item of report.diagnostics)
|
|
286
293
|
process.stderr.write(`${shown(report.path)}:${item.line}:${item.column}: ${item.severity} ${item.rule}: ${item.message}\n`);
|