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 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
- and the engine, configuration, and plugins in `engine.test.ts`.
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.0-rc.3
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;
@@ -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
- process.stderr.write(`mdtools: warning: ${skippedMessage(item, shown)}\n`);
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(createTwoFilesPatch(`a/${shown(change.file)}`, `b/${shown(change.file)}`, change.before, change.after));
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`);