@young1lin/dsh-ui-gitworkbench 0.1.5 → 0.1.7
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 +43 -0
- package/CHANGELOG_EN.md +43 -0
- package/lib/apply-blocks.js +159 -0
- package/lib/atomic-json.js +23 -5
- package/lib/blame.js +83 -0
- package/lib/client.js +34960 -11826
- package/lib/git-ops.js +25 -0
- package/lib/image-sniff.js +197 -0
- package/lib/index.js +401 -7
- package/lib/patch-model.js +223 -0
- package/lib/side-guard.js +55 -0
- package/lib/write-checked.js +164 -0
- package/package.json +7 -1
- package/src/apply-blocks.ts +215 -0
- package/src/atomic-json.ts +29 -5
- package/src/blame.ts +94 -0
- package/src/client/CodeEditor.tsx +317 -0
- package/src/client/FileBrowser.tsx +657 -0
- package/src/client/GitWorkbenchPanel.module.css +491 -12
- package/src/client/GitWorkbenchPanel.tsx +1655 -190
- package/src/client/ImageView.tsx +120 -0
- package/src/client/blame-gutter.ts +108 -0
- package/src/client/blame-view.ts +104 -0
- package/src/client/cm-diff.ts +108 -0
- package/src/client/cm-tokens.ts +79 -0
- package/src/client/diff-nav.ts +198 -0
- package/src/client/file-icon.ts +190 -0
- package/src/client/file-rows.ts +184 -0
- package/src/client/files-place.ts +178 -0
- package/src/client/glyphs.tsx +86 -0
- package/src/client/highlight.ts +25 -0
- package/src/client/history-layout.ts +52 -0
- package/src/client/idle-value.ts +53 -0
- package/src/client/image-view.ts +106 -0
- package/src/client/indent.ts +74 -0
- package/src/client/index.ts +59 -0
- package/src/client/locales.ts +179 -4
- package/src/client/pane-size.ts +71 -0
- package/src/client/side-edit.ts +244 -0
- package/src/client/side-rows.ts +258 -0
- package/src/client/stable-list.ts +31 -0
- package/src/client/use-change-nav.ts +83 -0
- package/src/client/worktree-view.ts +11 -1
- package/src/git-ops.ts +36 -1
- package/src/image-sniff.ts +204 -0
- package/src/index.ts +447 -7
- package/src/patch-model.ts +267 -0
- package/src/side-guard.ts +58 -0
- package/src/write-checked.ts +223 -0
package/lib/git-ops.js
CHANGED
|
@@ -399,6 +399,31 @@ export function countBufferLines(bytes) {
|
|
|
399
399
|
export function isBinaryPrefix(bytes, windowBytes) {
|
|
400
400
|
return bytes.subarray(0, windowBytes).includes(0);
|
|
401
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Whether a buffer is valid UTF-8, and so survives a decode/encode round trip.
|
|
404
|
+
*
|
|
405
|
+
* The editable pane hands the browser a decoded string and writes back what
|
|
406
|
+
* comes home encoded as UTF-8. For a file in any other encoding — GBK, Shift
|
|
407
|
+
* JIS, Latin-1 — that trip is LOSSY: every byte the decoder cannot read
|
|
408
|
+
* becomes U+FFFD, and writing the result replaces every non-ASCII byte in the
|
|
409
|
+
* file, including the lines nobody edited. Such a file carries no NUL byte,
|
|
410
|
+
* so the binary sniff above waves it through; only decoding it says so.
|
|
411
|
+
*
|
|
412
|
+
* `fatal` makes the decoder throw on the first invalid sequence rather than
|
|
413
|
+
* substituting, which is the whole question asked in one call and without
|
|
414
|
+
* allocating the string twice to compare it.
|
|
415
|
+
*
|
|
416
|
+
* @param bytes - the file's contents.
|
|
417
|
+
*/
|
|
418
|
+
export function decodesAsUtf8(bytes) {
|
|
419
|
+
try {
|
|
420
|
+
new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
402
427
|
/**
|
|
403
428
|
* Clip a diff to a character cap, and SAY so when the clip happened — a
|
|
404
429
|
* silently shortened diff reads as a complete one (TESTS.md H1).
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this file actually an image, and which kind?
|
|
3
|
+
*
|
|
4
|
+
* The extension is a hint, never the answer: `.png` is a filename, not a
|
|
5
|
+
* format, and a repository full of generated assets has mislabelled files in
|
|
6
|
+
* it. So the bytes decide — every format below is identified by the signature
|
|
7
|
+
* its own specification mandates in the first few bytes.
|
|
8
|
+
*
|
|
9
|
+
* The obvious alternative is the `file-type` package, which is the mature
|
|
10
|
+
* packaging of exactly this idea. It is not used here for three reasons: it
|
|
11
|
+
* is four transitive dependencies in a package that is published to npm, it
|
|
12
|
+
* identifies two hundred types where this needs eight, and — the deciding one
|
|
13
|
+
* — it could not make the render any safer than it already is. Sniffing is
|
|
14
|
+
* the FIRST of two gates; the second is the browser's own image decoder, and
|
|
15
|
+
* that one is authoritative in a way no table can be. If the trade ever turns,
|
|
16
|
+
* `sniffImage` is the only function to replace.
|
|
17
|
+
*
|
|
18
|
+
* Membership in the table is decided by one rule: browsers render it in an
|
|
19
|
+
* `<img>` element. That is why TIFF and HEIC are absent — recognising them
|
|
20
|
+
* would only let the view promise a picture it cannot draw, and "binary file"
|
|
21
|
+
* is the more honest answer for a format the reader's browser will refuse.
|
|
22
|
+
*
|
|
23
|
+
* SVG is the one member with no magic number, because it is XML rather than a
|
|
24
|
+
* container. It is admitted anyway: rendering happens through `<img>`, which
|
|
25
|
+
* the HTML specification defines as a non-scripted context — script elements,
|
|
26
|
+
* event handlers and external references inside the document do not run and do
|
|
27
|
+
* not load. That property, not a sanitiser, is what makes it safe.
|
|
28
|
+
*
|
|
29
|
+
* Pure: no node, no fs, no git. `tests/image-sniff.test.ts` loads it directly.
|
|
30
|
+
*
|
|
31
|
+
* @module @young1lin/dsh-ui-gitworkbench/image-sniff
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Largest image handed to the browser, in bytes.
|
|
35
|
+
*
|
|
36
|
+
* This is a WIRE budget, not a rendering one. The bytes cross the RPC channel
|
|
37
|
+
* base64-encoded, which costs a third again on top, so a cap of four megabytes
|
|
38
|
+
* is a payload of five and a third — already the largest single message the
|
|
39
|
+
* drawer sends. Screenshots and icons, which is what repositories actually
|
|
40
|
+
* hold, sit two orders of magnitude below it.
|
|
41
|
+
*/
|
|
42
|
+
export const IMAGE_BYTE_CAP = 4_000_000;
|
|
43
|
+
/** How many leading bytes any signature below needs. */
|
|
44
|
+
const SNIFF_BYTES = 64;
|
|
45
|
+
/** How much of a text file is read looking for an SVG root element.
|
|
46
|
+
*
|
|
47
|
+
* Larger than any magic number needs because the prologue in front of that
|
|
48
|
+
* root is unbounded in principle: an XML declaration, a DOCTYPE with an
|
|
49
|
+
* internal subset of declarations, and any number of comments all come
|
|
50
|
+
* first, and real files use all three. */
|
|
51
|
+
const SVG_PROLOGUE_BYTES = 4096;
|
|
52
|
+
/** Do `bytes` begin with these byte values at `at`? */
|
|
53
|
+
function at(bytes, offset, signature) {
|
|
54
|
+
if (bytes.length < offset + signature.length)
|
|
55
|
+
return false;
|
|
56
|
+
for (let i = 0; i < signature.length; i += 1) {
|
|
57
|
+
if (bytes[offset + i] !== signature[i])
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** The bytes of an ASCII marker, so the tables read as the specs write them. */
|
|
63
|
+
function ascii(text) {
|
|
64
|
+
return [...text].map(ch => ch.charCodeAt(0));
|
|
65
|
+
}
|
|
66
|
+
/** Little-endian uint32 at `offset`, or -1 when the buffer is too short. */
|
|
67
|
+
function u32le(bytes, offset) {
|
|
68
|
+
if (bytes.length < offset + 4)
|
|
69
|
+
return -1;
|
|
70
|
+
return (bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24)) >>> 0;
|
|
71
|
+
}
|
|
72
|
+
/** DIB header sizes BMP has ever defined. A `BM` prefix alone is two bytes of
|
|
73
|
+
* evidence, which any text beginning "BM" would satisfy; the header size is
|
|
74
|
+
* what makes the match a bitmap. */
|
|
75
|
+
const BMP_HEADERS = [12, 16, 40, 52, 56, 64, 108, 124];
|
|
76
|
+
/** ISO base-media brands that carry a still image a browser will draw. */
|
|
77
|
+
const AVIF_BRANDS = ['avif', 'avis'];
|
|
78
|
+
/**
|
|
79
|
+
* Identify one file from its leading bytes.
|
|
80
|
+
*
|
|
81
|
+
* @param bytes - the file's content, or at least its first {@link SNIFF_BYTES}.
|
|
82
|
+
* @returns what it is, or null for anything not in the table.
|
|
83
|
+
*/
|
|
84
|
+
export function sniffImage(bytes) {
|
|
85
|
+
if (bytes.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
// PNG: the eight-byte signature from the specification, chosen there
|
|
88
|
+
// precisely so that no other format collides with it.
|
|
89
|
+
if (at(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
|
90
|
+
return { mime: 'image/png', kind: 'PNG' };
|
|
91
|
+
}
|
|
92
|
+
// JPEG: SOI marker, then the first marker of the next segment.
|
|
93
|
+
if (at(bytes, 0, [0xff, 0xd8, 0xff])) {
|
|
94
|
+
return { mime: 'image/jpeg', kind: 'JPEG' };
|
|
95
|
+
}
|
|
96
|
+
if (at(bytes, 0, ascii('GIF87a')) || at(bytes, 0, ascii('GIF89a'))) {
|
|
97
|
+
return { mime: 'image/gif', kind: 'GIF' };
|
|
98
|
+
}
|
|
99
|
+
// WebP is a RIFF container; the four bytes after the length field are what
|
|
100
|
+
// separate it from a WAV or an AVI.
|
|
101
|
+
if (at(bytes, 0, ascii('RIFF')) && at(bytes, 8, ascii('WEBP'))) {
|
|
102
|
+
return { mime: 'image/webp', kind: 'WebP' };
|
|
103
|
+
}
|
|
104
|
+
// ISO base media: `ftyp` box, then the brand. Also matches HEIC and MP4,
|
|
105
|
+
// which is why only the still-image AVIF brands are admitted.
|
|
106
|
+
if (at(bytes, 4, ascii('ftyp')) && AVIF_BRANDS.some(brand => at(bytes, 8, ascii(brand)))) {
|
|
107
|
+
return { mime: 'image/avif', kind: 'AVIF' };
|
|
108
|
+
}
|
|
109
|
+
if (at(bytes, 0, ascii('BM')) && BMP_HEADERS.includes(u32le(bytes, 14))) {
|
|
110
|
+
return { mime: 'image/bmp', kind: 'BMP' };
|
|
111
|
+
}
|
|
112
|
+
// ICO: reserved zero, type 1 (icon) or 2 (cursor), then a non-zero count of
|
|
113
|
+
// images. The count is the check that a run of zero bytes cannot pass.
|
|
114
|
+
if (at(bytes, 0, [0x00, 0x00, 0x01, 0x00]) && (bytes[4] | (bytes[5] << 8)) > 0) {
|
|
115
|
+
return { mime: 'image/x-icon', kind: 'ICO' };
|
|
116
|
+
}
|
|
117
|
+
if (looksLikeSvg(bytes)) {
|
|
118
|
+
return { mime: 'image/svg+xml', kind: 'SVG' };
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Does this text begin an SVG document?
|
|
124
|
+
*
|
|
125
|
+
* Structural rather than a substring search: the prologue XML allows before a
|
|
126
|
+
* root element is skipped, and then the root element itself must be `svg`. A
|
|
127
|
+
* file that merely CONTAINS `<svg` somewhere — an HTML page with an inline
|
|
128
|
+
* icon, a TypeScript file with a template literal — is not one.
|
|
129
|
+
*/
|
|
130
|
+
function looksLikeSvg(bytes) {
|
|
131
|
+
const head = bytes.subarray(0, SVG_PROLOGUE_BYTES);
|
|
132
|
+
// A NUL rules out text before any parsing: the same test the diff side uses
|
|
133
|
+
// to call a file binary.
|
|
134
|
+
if (head.includes(0))
|
|
135
|
+
return false;
|
|
136
|
+
let text;
|
|
137
|
+
try {
|
|
138
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(head);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// A truncated multi-byte character at the cut is not a decode failure of
|
|
142
|
+
// the FILE, so retry lenient; a genuinely non-UTF-8 file yields U+FFFD,
|
|
143
|
+
// which no prologue below accepts.
|
|
144
|
+
text = new TextDecoder('utf-8').decode(head);
|
|
145
|
+
}
|
|
146
|
+
// Strip a byte-order mark, which is legal before an XML declaration.
|
|
147
|
+
let rest = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
148
|
+
for (;;) {
|
|
149
|
+
rest = rest.replace(/^\s+/, '');
|
|
150
|
+
if (rest.startsWith('<?')) {
|
|
151
|
+
const end = rest.indexOf('?>');
|
|
152
|
+
// Prologue cut off by the sniff window: undecidable, so not an image.
|
|
153
|
+
if (end === -1)
|
|
154
|
+
return false;
|
|
155
|
+
rest = rest.slice(end + 2);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (rest.startsWith('<!--')) {
|
|
159
|
+
const end = rest.indexOf('-->');
|
|
160
|
+
if (end === -1)
|
|
161
|
+
return false;
|
|
162
|
+
rest = rest.slice(end + 3);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (rest.startsWith('<!')) {
|
|
166
|
+
// A DOCTYPE may carry an internal subset in brackets, and the
|
|
167
|
+
// declarations inside it end with '>' characters of their own —
|
|
168
|
+
// scanning to the first one lands in the middle of the subset and the
|
|
169
|
+
// root element is never reached. Real files do this: matplotlib ships
|
|
170
|
+
// an SVG whose DOCTYPE declares an ATTLIST, and it was the one file in
|
|
171
|
+
// a hundred and twenty-five thousand that this missed.
|
|
172
|
+
const bracket = rest.indexOf('[');
|
|
173
|
+
const end = rest.indexOf('>');
|
|
174
|
+
if (end === -1)
|
|
175
|
+
return false;
|
|
176
|
+
if (bracket === -1 || bracket > end) {
|
|
177
|
+
rest = rest.slice(end + 1);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const closed = rest.indexOf(']', bracket);
|
|
181
|
+
if (closed === -1)
|
|
182
|
+
return false;
|
|
183
|
+
const after = rest.indexOf('>', closed);
|
|
184
|
+
if (after === -1)
|
|
185
|
+
return false;
|
|
186
|
+
rest = rest.slice(after + 1);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
// The root element, and only `svg`: the character after the name must end
|
|
192
|
+
// it, so `<svgfoo>` is not a match.
|
|
193
|
+
if (!rest.startsWith('<svg'))
|
|
194
|
+
return false;
|
|
195
|
+
const after = rest.charAt(4);
|
|
196
|
+
return after === '' || after === '>' || after === '/' || /\s/.test(after);
|
|
197
|
+
}
|