@kgdiff/cli 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +211 -2
- package/dist/apply-command.d.ts +34 -0
- package/dist/apply-command.d.ts.map +1 -0
- package/dist/apply-command.js +137 -0
- package/dist/apply-command.js.map +1 -0
- package/dist/args.d.ts +96 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +463 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +17 -0
- package/dist/bin.js.map +1 -0
- package/dist/diff-command.d.ts +29 -0
- package/dist/diff-command.d.ts.map +1 -0
- package/dist/diff-command.js +236 -0
- package/dist/diff-command.js.map +1 -0
- package/dist/errors.d.ts +48 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +92 -0
- package/dist/errors.js.map +1 -0
- package/dist/help.d.ts +11 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +247 -0
- package/dist/help.js.map +1 -0
- package/dist/html.d.ts +53 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +107 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs.d.ts +54 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +209 -0
- package/dist/inputs.js.map +1 -0
- package/dist/io.d.ts +64 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +2 -0
- package/dist/io.js.map +1 -0
- package/dist/locale.d.ts +58 -0
- package/dist/locale.d.ts.map +1 -0
- package/dist/locale.js +121 -0
- package/dist/locale.js.map +1 -0
- package/dist/main.d.ts +10 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +51 -0
- package/dist/main.js.map +1 -0
- package/dist/markdown.d.ts +30 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +93 -0
- package/dist/markdown.js.map +1 -0
- package/dist/model.d.ts +63 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +151 -0
- package/dist/model.js.map +1 -0
- package/dist/node-io.d.ts +12 -0
- package/dist/node-io.d.ts.map +1 -0
- package/dist/node-io.js +135 -0
- package/dist/node-io.js.map +1 -0
- package/dist/output.d.ts +27 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +50 -0
- package/dist/output.js.map +1 -0
- package/dist/overwrite.d.ts +14 -0
- package/dist/overwrite.d.ts.map +1 -0
- package/dist/overwrite.js +55 -0
- package/dist/overwrite.js.map +1 -0
- package/dist/report.d.ts +52 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +64 -0
- package/dist/report.js.map +1 -0
- package/dist/style.d.ts +46 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +140 -0
- package/dist/style.js.map +1 -0
- package/dist/summary.d.ts +40 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +352 -0
- package/dist/summary.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/dist/width.d.ts +45 -0
- package/dist/width.d.ts.map +1 -0
- package/dist/width.js +232 -0
- package/dist/width.js.map +1 -0
- package/package.json +56 -3
- package/src/apply-command.ts +203 -0
- package/src/args.ts +605 -0
- package/src/bin.ts +22 -0
- package/src/diff-command.ts +299 -0
- package/src/errors.ts +131 -0
- package/src/help.ts +286 -0
- package/src/html.ts +145 -0
- package/src/index.ts +71 -0
- package/src/inputs.ts +269 -0
- package/src/io.ts +68 -0
- package/src/locale.ts +177 -0
- package/src/main.ts +56 -0
- package/src/markdown.ts +119 -0
- package/src/model.ts +181 -0
- package/src/node-io.ts +146 -0
- package/src/output.ts +81 -0
- package/src/overwrite.ts +75 -0
- package/src/report.ts +101 -0
- package/src/style.ts +164 -0
- package/src/summary.ts +440 -0
- package/src/version.ts +21 -0
- package/src/width.ts +222 -0
package/src/width.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal display width of text (East Asian Width, combining marks, emoji) and the helpers
|
|
3
|
+
* built on it: sanitizing untrusted labels, truncating with an ellipsis and padding.
|
|
4
|
+
*
|
|
5
|
+
* Widths follow what modern terminals render: CJK / fullwidth / emoji-presentation characters
|
|
6
|
+
* take two columns, combining marks, joiners and variation selectors none, East Asian
|
|
7
|
+
* "ambiguous" characters (`→`, `·`, `…`, box drawing) one. Measurement is per grapheme cluster
|
|
8
|
+
* (`Intl.Segmenter`), so `é` (e + U+0301), a ZWJ emoji sequence, a flag and a keycap
|
|
9
|
+
* are each measured as one unit.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** East Asian Width W and F ranges (Unicode 16), as inclusive [start, end] pairs, sorted. */
|
|
13
|
+
const WIDE_RANGES: readonly number[] = [
|
|
14
|
+
0x1100, 0x115f, 0x231a, 0x231b, 0x2329, 0x232a, 0x23e9, 0x23ec, 0x23f0, 0x23f0, 0x23f3, 0x23f3,
|
|
15
|
+
0x25fd, 0x25fe, 0x2614, 0x2615, 0x2630, 0x2637, 0x2648, 0x2653, 0x267f, 0x267f, 0x268a, 0x268f,
|
|
16
|
+
0x2693, 0x2693, 0x26a1, 0x26a1, 0x26aa, 0x26ab, 0x26bd, 0x26be, 0x26c4, 0x26c5, 0x26ce, 0x26ce,
|
|
17
|
+
0x26d4, 0x26d4, 0x26ea, 0x26ea, 0x26f2, 0x26f3, 0x26f5, 0x26f5, 0x26fa, 0x26fa, 0x26fd, 0x26fd,
|
|
18
|
+
0x2705, 0x2705, 0x270a, 0x270b, 0x2728, 0x2728, 0x274c, 0x274c, 0x274e, 0x274e, 0x2753, 0x2755,
|
|
19
|
+
0x2757, 0x2757, 0x2795, 0x2797, 0x27b0, 0x27b0, 0x27bf, 0x27bf, 0x2b1b, 0x2b1c, 0x2b50, 0x2b50,
|
|
20
|
+
0x2b55, 0x2b55, 0x2e80, 0x2e99, 0x2e9b, 0x2ef3, 0x2f00, 0x2fd5, 0x2ff0, 0x303e, 0x3041, 0x3096,
|
|
21
|
+
0x3099, 0x30ff, 0x3105, 0x312f, 0x3131, 0x318e, 0x3190, 0x31e5, 0x31ef, 0x321e, 0x3220, 0x3247,
|
|
22
|
+
0x3250, 0xa48c, 0xa490, 0xa4c6, 0xa960, 0xa97c, 0xac00, 0xd7a3, 0xf900, 0xfaff, 0xfe10, 0xfe19,
|
|
23
|
+
0xfe30, 0xfe52, 0xfe54, 0xfe66, 0xfe68, 0xfe6b, 0xff01, 0xff60, 0xffe0, 0xffe6, 0x16fe0, 0x16fe4,
|
|
24
|
+
0x16ff0, 0x16ff1, 0x17000, 0x187f7, 0x18800, 0x18cd5, 0x18d00, 0x18d08, 0x1aff0, 0x1aff3, 0x1aff5,
|
|
25
|
+
0x1affb, 0x1affd, 0x1affe, 0x1b000, 0x1b122, 0x1b132, 0x1b132, 0x1b150, 0x1b152, 0x1b155, 0x1b155,
|
|
26
|
+
0x1b164, 0x1b167, 0x1b170, 0x1b2fb, 0x1d300, 0x1d356, 0x1d360, 0x1d376, 0x1f004, 0x1f004, 0x1f0cf,
|
|
27
|
+
0x1f0cf, 0x1f18e, 0x1f18e, 0x1f191, 0x1f19a, 0x1f200, 0x1f202, 0x1f210, 0x1f23b, 0x1f240, 0x1f248,
|
|
28
|
+
0x1f250, 0x1f251, 0x1f260, 0x1f265, 0x1f300, 0x1f320, 0x1f32d, 0x1f335, 0x1f337, 0x1f37c, 0x1f37e,
|
|
29
|
+
0x1f393, 0x1f3a0, 0x1f3ca, 0x1f3cf, 0x1f3d3, 0x1f3e0, 0x1f3f0, 0x1f3f4, 0x1f3f4, 0x1f3f8, 0x1f43e,
|
|
30
|
+
0x1f440, 0x1f440, 0x1f442, 0x1f4fc, 0x1f4ff, 0x1f53d, 0x1f54b, 0x1f54e, 0x1f550, 0x1f567, 0x1f57a,
|
|
31
|
+
0x1f57a, 0x1f595, 0x1f596, 0x1f5a4, 0x1f5a4, 0x1f5fb, 0x1f64f, 0x1f680, 0x1f6c5, 0x1f6cc, 0x1f6cc,
|
|
32
|
+
0x1f6d0, 0x1f6d2, 0x1f6d5, 0x1f6d7, 0x1f6dc, 0x1f6df, 0x1f6eb, 0x1f6ec, 0x1f6f4, 0x1f6fc, 0x1f7e0,
|
|
33
|
+
0x1f7eb, 0x1f7f0, 0x1f7f0, 0x1f90c, 0x1f93a, 0x1f93c, 0x1f945, 0x1f947, 0x1f9ff, 0x1fa70, 0x1fa7c,
|
|
34
|
+
0x1fa80, 0x1fa89, 0x1fa8f, 0x1fac6, 0x1face, 0x1fadc, 0x1fadf, 0x1fae9, 0x1faf0, 0x1faf8, 0x20000,
|
|
35
|
+
0x2fffd, 0x30000, 0x3fffd,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** True when the code point is East Asian Wide or Fullwidth. O(log n). */
|
|
39
|
+
export function isWideCodePoint(cp: number): boolean {
|
|
40
|
+
if (cp < 0x1100) return false;
|
|
41
|
+
let lo = 0;
|
|
42
|
+
let hi = WIDE_RANGES.length / 2 - 1;
|
|
43
|
+
while (lo <= hi) {
|
|
44
|
+
const mid = (lo + hi) >> 1;
|
|
45
|
+
const start = WIDE_RANGES[mid * 2] as number;
|
|
46
|
+
const end = WIDE_RANGES[mid * 2 + 1] as number;
|
|
47
|
+
if (cp < start) hi = mid - 1;
|
|
48
|
+
else if (cp > end) lo = mid + 1;
|
|
49
|
+
else return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
|
|
55
|
+
const EMOJI_PRESENTATION = /\p{Emoji_Presentation}/u;
|
|
56
|
+
const EMOJI = /\p{Emoji}/u;
|
|
57
|
+
const REGIONAL_INDICATOR = /\p{Regional_Indicator}/u;
|
|
58
|
+
|
|
59
|
+
/** Display width of one grapheme cluster: 0, 1 or 2 columns. */
|
|
60
|
+
export function graphemeWidth(grapheme: string): number {
|
|
61
|
+
const cp = grapheme.codePointAt(0);
|
|
62
|
+
if (cp === undefined) return 0;
|
|
63
|
+
if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
64
|
+
if (cp < 0x7f && grapheme.length === 1) return 1; // ASCII fast path
|
|
65
|
+
if (grapheme.length > 1) {
|
|
66
|
+
// Emoji sequences: presentation selector, ZWJ sequences, flags, keycaps, skin tones.
|
|
67
|
+
if (grapheme.includes('\uFE0F') && EMOJI.test(grapheme)) return 2;
|
|
68
|
+
if (grapheme.includes('\u200D') && EMOJI_PRESENTATION.test(grapheme)) return 2;
|
|
69
|
+
if (REGIONAL_INDICATOR.test(grapheme)) return 2;
|
|
70
|
+
}
|
|
71
|
+
const first = String.fromCodePoint(cp);
|
|
72
|
+
if (ZERO_WIDTH.test(first)) return 0;
|
|
73
|
+
if (grapheme.includes('\uFE0E')) return 1; // text presentation selector
|
|
74
|
+
if (isWideCodePoint(cp) || EMOJI_PRESENTATION.test(first)) return 2;
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let segmenter: Intl.Segmenter | null | undefined;
|
|
79
|
+
|
|
80
|
+
/** Grapheme clusters of `text` (code points as a fallback when `Intl.Segmenter` is missing). */
|
|
81
|
+
export function graphemes(text: string): string[] {
|
|
82
|
+
if (segmenter === undefined) {
|
|
83
|
+
segmenter =
|
|
84
|
+
typeof Intl.Segmenter === 'function'
|
|
85
|
+
? new Intl.Segmenter('en', { granularity: 'grapheme' })
|
|
86
|
+
: null;
|
|
87
|
+
}
|
|
88
|
+
if (segmenter) return Array.from(segmenter.segment(text), (s) => s.segment);
|
|
89
|
+
// Fallback: attach zero-width code points to the previous one.
|
|
90
|
+
const out: string[] = [];
|
|
91
|
+
for (const ch of text) {
|
|
92
|
+
const cp = ch.codePointAt(0) as number;
|
|
93
|
+
if (out.length > 0 && (ZERO_WIDTH.test(ch) || (cp >= 0x1f3fb && cp <= 0x1f3ff))) {
|
|
94
|
+
out[out.length - 1] += ch;
|
|
95
|
+
} else out.push(ch);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
101
|
+
|
|
102
|
+
/** Display width of `text` in terminal columns. Text must not contain ANSI escape sequences. */
|
|
103
|
+
export function stringWidth(text: string): number {
|
|
104
|
+
if (PRINTABLE_ASCII.test(text)) return text.length;
|
|
105
|
+
let width = 0;
|
|
106
|
+
for (const g of graphemes(text)) width += graphemeWidth(g);
|
|
107
|
+
return width;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching control characters is the point.
|
|
111
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invisible characters that can disguise text ("Trojan Source"): bidirectional controls
|
|
115
|
+
* (U+061C, U+200E/F, U+202A–U+202E, U+2066–U+2069), zero-width and invisible format characters
|
|
116
|
+
* (U+180E, U+200B, U+2060–U+2064, U+FEFF, U+FFF9–U+FFFB) and tag characters outside emoji flag
|
|
117
|
+
* sequences. Joiners (U+200C, U+200D) and variation selectors stay: scripts and emoji need them.
|
|
118
|
+
*/
|
|
119
|
+
const INVISIBLE =
|
|
120
|
+
/[\u061C\u180E\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF\uFFF9-\uFFFB]|(?<!\u{1F3F4}[\u{E0020}-\u{E007E}]*)[\u{E0000}-\u{E007F}]/gu;
|
|
121
|
+
|
|
122
|
+
/** Replace invisible and bidirectional control characters with a visible `⟨U+202E⟩`. */
|
|
123
|
+
export function revealInvisible(text: string): string {
|
|
124
|
+
return text.replace(INVISIBLE, (ch) => {
|
|
125
|
+
const cp = ch.codePointAt(0) as number;
|
|
126
|
+
return `⟨U+${cp.toString(16).toUpperCase().padStart(4, '0')}⟩`;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Make untrusted text (labels, ids, file names, parser messages) safe to print on one
|
|
132
|
+
* terminal line: tabs and line breaks become spaces, other C0 controls (notably ESC, which
|
|
133
|
+
* could inject escape sequences or hyperlinks) become control pictures (`␛`), C1 controls
|
|
134
|
+
* U+FFFD, and invisible or bidirectional controls a visible `⟨U+202E⟩`.
|
|
135
|
+
*/
|
|
136
|
+
export function sanitize(text: string): string {
|
|
137
|
+
return revealInvisible(
|
|
138
|
+
text.replace(CONTROL, (ch) => {
|
|
139
|
+
const cp = ch.charCodeAt(0);
|
|
140
|
+
if (cp === 0x09 || cp === 0x0a || cp === 0x0d || cp === 0x2028 || cp === 0x2029) return ' ';
|
|
141
|
+
if (cp < 0x20) return String.fromCharCode(0x2400 + cp);
|
|
142
|
+
if (cp === 0x7f) return '\u2421';
|
|
143
|
+
return '\uFFFD';
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `sanitize` line by line, keeping line breaks (stack traces). */
|
|
149
|
+
export function sanitizeLines(text: string): string {
|
|
150
|
+
return text.split(/\r?\n/).map(sanitize).join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Truncate `text` to at most `max` columns, ending with `…` when something was cut. Never
|
|
155
|
+
* splits a grapheme; a wide character that does not fit is dropped whole (the result may then
|
|
156
|
+
* be one column narrower than `max`). Returns '' for `max` ≤ 0.
|
|
157
|
+
*/
|
|
158
|
+
export function truncate(text: string, max: number, ellipsis = '…'): string {
|
|
159
|
+
if (max <= 0) return '';
|
|
160
|
+
if (stringWidth(text) <= max) return text;
|
|
161
|
+
const room = max - stringWidth(ellipsis);
|
|
162
|
+
if (room < 0) return '';
|
|
163
|
+
let width = 0;
|
|
164
|
+
let out = '';
|
|
165
|
+
for (const g of graphemes(text)) {
|
|
166
|
+
const w = graphemeWidth(g);
|
|
167
|
+
if (width + w > room) break;
|
|
168
|
+
out += g;
|
|
169
|
+
width += w;
|
|
170
|
+
}
|
|
171
|
+
return out.trimEnd() + ellipsis;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Pad `text` with spaces on the right to `width` columns (no-op when already wider). */
|
|
175
|
+
export function padEnd(text: string, width: number): string {
|
|
176
|
+
const gap = width - stringWidth(text);
|
|
177
|
+
return gap > 0 ? text + ' '.repeat(gap) : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Pad `text` with spaces on the left to `width` columns (no-op when already wider). */
|
|
181
|
+
export function padStart(text: string, width: number): string {
|
|
182
|
+
const gap = width - stringWidth(text);
|
|
183
|
+
return gap > 0 ? ' '.repeat(gap) + text : text;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Greedy word wrap to `width` columns. Breaks at spaces; CJK text (no spaces) breaks between
|
|
188
|
+
* graphemes. Lines never exceed `width` unless a single grapheme is wider.
|
|
189
|
+
*/
|
|
190
|
+
export function wrap(text: string, width: number): string[] {
|
|
191
|
+
if (width <= 0 || stringWidth(text) <= width) return [text];
|
|
192
|
+
const lines: string[] = [];
|
|
193
|
+
let line = '';
|
|
194
|
+
let lineWidth = 0;
|
|
195
|
+
const flush = (): void => {
|
|
196
|
+
lines.push(line.trimEnd());
|
|
197
|
+
line = '';
|
|
198
|
+
lineWidth = 0;
|
|
199
|
+
};
|
|
200
|
+
for (const word of text.split(/(?<= )/)) {
|
|
201
|
+
const w = stringWidth(word);
|
|
202
|
+
if (lineWidth + stringWidth(word.trimEnd()) <= width) {
|
|
203
|
+
line += word;
|
|
204
|
+
lineWidth += w;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (lineWidth > 0) flush();
|
|
208
|
+
if (stringWidth(word.trimEnd()) <= width) {
|
|
209
|
+
line = word;
|
|
210
|
+
lineWidth = w;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
for (const g of graphemes(word)) {
|
|
214
|
+
const gw = graphemeWidth(g);
|
|
215
|
+
if (lineWidth + gw > width && lineWidth > 0) flush();
|
|
216
|
+
line += g;
|
|
217
|
+
lineWidth += gw;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (line.trim()) lines.push(line.trimEnd());
|
|
221
|
+
return lines;
|
|
222
|
+
}
|