@rightkit/document-core 0.1.0 → 0.1.2
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 +17 -0
- package/dist/documents/pdf/usePdfSavePlan.js +7 -23
- package/dist/documents/toolbarPrimitives.js +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/dist/settings/sections/MacFileAssociationsBlock.js +2 -2
- package/dist/settings/themes.js +9 -1
- package/dist/state/tauriBridge.d.ts +0 -14
- package/dist/state/tauriBridge.js +4 -137
- package/package.json +10 -12
- package/dist/state/browserQa.d.ts +0 -21
- package/dist/state/browserQa.js +0 -303
- package/dist/state/browserQaDocx.d.ts +0 -3
- package/dist/state/browserQaDocx.js +0 -35
- package/dist/state/browserQaLicense.d.ts +0 -8
- package/dist/state/browserQaLicense.js +0 -7
- package/dist/state/browserQaPdf.d.ts +0 -60
- package/dist/state/browserQaPdf.js +0 -302
- package/dist/state/browserQaText.d.ts +0 -6
- package/dist/state/browserQaText.js +0 -48
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic PDF-edit-command mocks for browser QA mode (`VITE_RIGHTKIT_BROWSER_QA=1`,
|
|
3
|
-
* `?qa=1&doc=pdf`). Never active in production builds — every export here is only ever reached
|
|
4
|
-
* through `tauriBridge.ts`'s `isBrowserQaMode()` gate, exactly like `qaOcrLines` already is for
|
|
5
|
-
* OCR (see `browserQa.ts`).
|
|
6
|
-
*
|
|
7
|
-
* Before this module, `requireTauriForPdfEdit` threw `unsupported_outside_tauri` for every
|
|
8
|
-
* PDF-editor command (`inspect_pdf_page`, `compose_pdf_preview`, `prepare_pdf_save`,
|
|
9
|
-
* `commit_pdf_save`, `list_pdf_fonts`), so Save and Edit-text had zero coverage in the automated
|
|
10
|
-
* `qa:functional:browser` harness — every PDF save regression had to be found by hand in the
|
|
11
|
-
* native app. This module gives the harness a real, in-memory PDF "disk" and mirrors the Rust
|
|
12
|
-
* save-protocol rules (`src-tauri/crates/pdf/src/save.rs`'s `select_compose_base`, the stale-sha
|
|
13
|
-
* check, single-use tokens) closely enough that ANNOTATION editing + save (stamp/ink/highlight, via
|
|
14
|
-
* pdf.js's own `AnnotationEditorLayer`/`saveDocument()`) is now exercised end to end in the
|
|
15
|
-
* headless browser harness. Source-TEXT editing (`inspect_pdf_page`'s `islands`) stays native-only:
|
|
16
|
-
* browser QA has no Rust content-stream walker, so `qaInspectPdfPage` always reports zero editable
|
|
17
|
-
* islands — good enough to keep the Edit-text UI reachable/non-crashing, not to exercise a real
|
|
18
|
-
* text-replace round trip.
|
|
19
|
-
*/
|
|
20
|
-
import { qaDocumentForPath } from "./browserQa.js";
|
|
21
|
-
// -------------------------------------------------------------------------------------------
|
|
22
|
-
// byte/base64/sha helpers
|
|
23
|
-
// -------------------------------------------------------------------------------------------
|
|
24
|
-
/** One JS UTF-16 code unit per byte value (0-255) — the "binary string" `btoa` expects, and
|
|
25
|
-
* exactly the "latin1 string of bytes" `window.__viewrightQa.pdfDiskLatin1` exposes so QA
|
|
26
|
-
* assertions can `.includes("/Subtype /Stamp")` etc. against raw (uncompressed) PDF syntax. */
|
|
27
|
-
function bytesToBinaryString(bytes) {
|
|
28
|
-
let out = "";
|
|
29
|
-
const chunk = 0x8000;
|
|
30
|
-
for (let i = 0; i < bytes.length; i += chunk) {
|
|
31
|
-
const slice = bytes.subarray(i, i + chunk);
|
|
32
|
-
for (let j = 0; j < slice.length; j += 1)
|
|
33
|
-
out += String.fromCharCode(slice[j]);
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
function bytesToBase64(bytes) {
|
|
38
|
-
return btoa(bytesToBinaryString(bytes));
|
|
39
|
-
}
|
|
40
|
-
function base64ToBytes(base64) {
|
|
41
|
-
const binary = atob(base64);
|
|
42
|
-
const bytes = new Uint8Array(binary.length);
|
|
43
|
-
for (let i = 0; i < binary.length; i += 1)
|
|
44
|
-
bytes[i] = binary.charCodeAt(i);
|
|
45
|
-
return bytes;
|
|
46
|
-
}
|
|
47
|
-
/** `qaDocumentForPath`'s `assetUrl` is always a `data:...;base64,<payload>` URL (the static QA
|
|
48
|
-
* fixture constant, or a corpus file re-encoded by `bytesToBase64PdfUrl`) — decode straight to
|
|
49
|
-
* bytes rather than going through a real `fetch(dataUrl)` round trip. */
|
|
50
|
-
function dataUrlToBytes(dataUrl) {
|
|
51
|
-
const comma = dataUrl.indexOf(",");
|
|
52
|
-
if (comma === -1)
|
|
53
|
-
throw new Error("qa pdf fixture: not a data: URL");
|
|
54
|
-
return base64ToBytes(dataUrl.slice(comma + 1));
|
|
55
|
-
}
|
|
56
|
-
/** Matches `document_sha256` (`src-tauri/crates/pdf/src/model.rs`): plain SHA-256 of the whole
|
|
57
|
-
* byte sequence, lowercase hex. */
|
|
58
|
-
export async function sha256Hex(bytes) {
|
|
59
|
-
// `crypto.subtle.digest` hashes exactly a `BufferSource` view's byteOffset..byteLength range
|
|
60
|
-
// (not its whole backing buffer), and never transfers/detaches it — safe to pass a live reference
|
|
61
|
-
// into `qaDisk`. The `new Uint8Array(bytes)` copy is only here to satisfy `BufferSource`'s
|
|
62
|
-
// plain-`ArrayBuffer` (vs. `ArrayBufferLike`/`SharedArrayBuffer`) typing, not for safety.
|
|
63
|
-
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
64
|
-
return Array.from(new Uint8Array(digest))
|
|
65
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
66
|
-
.join("");
|
|
67
|
-
}
|
|
68
|
-
function startsWithBytes(haystack, prefix) {
|
|
69
|
-
if (haystack.length < prefix.length)
|
|
70
|
-
return false;
|
|
71
|
-
for (let i = 0; i < prefix.length; i += 1) {
|
|
72
|
-
if (haystack[i] !== prefix[i])
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
function affectedPagesFor(commands) {
|
|
78
|
-
const pages = new Set();
|
|
79
|
-
for (const command of commands) {
|
|
80
|
-
pages.add(command.kind === "replaceSourceText" ? command.locator.page : command.page);
|
|
81
|
-
}
|
|
82
|
-
return Array.from(pages).sort((a, b) => a - b);
|
|
83
|
-
}
|
|
84
|
-
// -------------------------------------------------------------------------------------------
|
|
85
|
-
// In-memory QA "disk"
|
|
86
|
-
// -------------------------------------------------------------------------------------------
|
|
87
|
-
const qaDisk = new Map();
|
|
88
|
-
/** Lazily seeds `qaDisk` for `path` from `qaDocumentForPath`'s fixture/corpus data URL on first
|
|
89
|
-
* touch, then always returns the (possibly since-committed) current bytes from the map. */
|
|
90
|
-
async function currentDiskBytes(path) {
|
|
91
|
-
const existing = qaDisk.get(path);
|
|
92
|
-
if (existing)
|
|
93
|
-
return existing;
|
|
94
|
-
const doc = await qaDocumentForPath(path);
|
|
95
|
-
if (!doc?.assetUrl)
|
|
96
|
-
return null;
|
|
97
|
-
const bytes = dataUrlToBytes(doc.assetUrl);
|
|
98
|
-
qaDisk.set(path, bytes);
|
|
99
|
-
return bytes;
|
|
100
|
-
}
|
|
101
|
-
/** Exported for PdfViewer.tsx's post-commit reload only (see its own call site's comment): the
|
|
102
|
-
* `url` prop it otherwise reloads from is a static `data:` URL captured once at document-open
|
|
103
|
-
* time — unlike a real Tauri stream token, it can never reflect a later commit on its own, so the
|
|
104
|
-
* reload needs to ask this in-memory disk for the CURRENT bytes directly. Returns `null` outside
|
|
105
|
-
* QA mode or for a path never touched through the PDF-edit bridge. */
|
|
106
|
-
export async function qaCurrentPdfBytes(path) {
|
|
107
|
-
return currentDiskBytes(path);
|
|
108
|
-
}
|
|
109
|
-
export async function qaCurrentPdfAssetUrl(path) {
|
|
110
|
-
const bytes = await qaCurrentPdfBytes(path);
|
|
111
|
-
return bytes ? `data:application/pdf;base64,${bytesToBase64(bytes)}` : null;
|
|
112
|
-
}
|
|
113
|
-
/** Test-only: clears the in-memory QA disk and save-token registry so each test starts from a
|
|
114
|
-
* fresh fixture seed, independent of what earlier tests in the same file committed. Mirrors
|
|
115
|
-
* `resourceCensus.ts`'s `resetResourceCensusForTests` convention. */
|
|
116
|
-
export function resetQaPdfStateForTests() {
|
|
117
|
-
qaDisk.clear();
|
|
118
|
-
qaSaveTokens.clear();
|
|
119
|
-
}
|
|
120
|
-
// -------------------------------------------------------------------------------------------
|
|
121
|
-
// inspect_pdf_page / list_pdf_fonts
|
|
122
|
-
// -------------------------------------------------------------------------------------------
|
|
123
|
-
export async function qaInspectPdfPage(path, page) {
|
|
124
|
-
const bytes = await currentDiskBytes(path);
|
|
125
|
-
if (!bytes)
|
|
126
|
-
throw { code: "malformed_content", message: `qa disk has no PDF at ${path}` };
|
|
127
|
-
const documentSha256 = await sha256Hex(bytes);
|
|
128
|
-
return {
|
|
129
|
-
documentSha256,
|
|
130
|
-
page,
|
|
131
|
-
// Matches the QA fixture's own `/MediaBox [0 0 612 792]` (both pages, US Letter) — a corpus
|
|
132
|
-
// PDF's real box would differ, but source-text editing (the only consumer of these bounds)
|
|
133
|
-
// never runs against a corpus fixture in the automated harness.
|
|
134
|
-
mediaBox: [0, 0, 612, 792],
|
|
135
|
-
cropBox: [0, 0, 612, 792],
|
|
136
|
-
rotation: 0,
|
|
137
|
-
islands: page === 1 ? [qaFixtureIsland(documentSha256)] : [],
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* One hand-built editable island for the QA fixture's page 1 title run `BT /F1 24 Tf 72 700 Td
|
|
142
|
-
* (Page 1: external link below) Tj ET` (Helvetica 24pt, baseline at y=700). The title run is
|
|
143
|
-
* deliberately chosen over the "Visit example.com" / "Go to page 2" runs below it because those
|
|
144
|
-
* two carry Link annotations whose `<a>` overlays would intercept the click before it reaches the
|
|
145
|
-
* page hit-test; the title has no annotation over it. Geometry uses Helvetica's nominal metrics
|
|
146
|
-
* (ascender 0.718em, descender 0.207em, ~0.5em average advance) so the inline editor's on-screen
|
|
147
|
-
* box lines up with the painted glyphs closely enough to dogfood click-to-edit UX (font face,
|
|
148
|
-
* size at zoom, glyph masking). The locator carries no spans: browser QA has no content-stream
|
|
149
|
-
* walker or composer, so a committed edit is staged but never composed into the bytes — the round
|
|
150
|
-
* trip that IS covered natively.
|
|
151
|
-
*/
|
|
152
|
-
function qaFixtureIsland(documentSha256) {
|
|
153
|
-
const x0 = 72;
|
|
154
|
-
const baseline = 700;
|
|
155
|
-
const sizePt = 24;
|
|
156
|
-
const text = "Page 1: external link below";
|
|
157
|
-
const width = text.length * sizePt * 0.5;
|
|
158
|
-
const top = baseline + sizePt * 0.718;
|
|
159
|
-
const bottom = baseline - sizePt * 0.207;
|
|
160
|
-
return {
|
|
161
|
-
id: "qa-island-visit-example",
|
|
162
|
-
text,
|
|
163
|
-
quads: [[x0, bottom, x0 + width, bottom, x0 + width, top, x0, top]],
|
|
164
|
-
baseline: [x0, baseline, x0 + width, baseline],
|
|
165
|
-
appearance: {
|
|
166
|
-
fontId: "base14:helvetica",
|
|
167
|
-
family: "Helvetica",
|
|
168
|
-
sizePt,
|
|
169
|
-
color: { r: 0, g: 0, b: 0, alpha: 1 },
|
|
170
|
-
},
|
|
171
|
-
locator: { documentSha256, page: 1, spans: [], islandDigest: "qa-fixture" },
|
|
172
|
-
editable: true,
|
|
173
|
-
rejection: null,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
export async function qaListPdfFonts() {
|
|
177
|
-
return [];
|
|
178
|
-
}
|
|
179
|
-
// -------------------------------------------------------------------------------------------
|
|
180
|
-
// compose_pdf_preview (§9.4 memory-only preview)
|
|
181
|
-
// -------------------------------------------------------------------------------------------
|
|
182
|
-
export async function qaComposePdfPreview(request) {
|
|
183
|
-
const bytes = await currentDiskBytes(request.sourcePath);
|
|
184
|
-
if (!bytes) {
|
|
185
|
-
throw { code: "malformed_content", message: `qa disk has no PDF at ${request.sourcePath}` };
|
|
186
|
-
}
|
|
187
|
-
const actualSha = await sha256Hex(bytes);
|
|
188
|
-
if (request.expectedSourceSha256 !== actualSha) {
|
|
189
|
-
throw { code: "stale_source", message: "PDF changed since edit started" };
|
|
190
|
-
}
|
|
191
|
-
// Source-text commands are never applied here (native-only, per module doc) — an empty command
|
|
192
|
-
// list is exactly what the real composer also does with no-op input (see `compose_candidate`'s
|
|
193
|
-
// own `if commands.is_empty() { return source.to_vec() }` fast path), so this only diverges from
|
|
194
|
-
// real behavior if a caller stages a non-empty `sourceCommands` list, which QA scenarios don't.
|
|
195
|
-
return { candidateBytes: bytes, affectedPages: affectedPagesFor(request.sourceCommands) };
|
|
196
|
-
}
|
|
197
|
-
// -------------------------------------------------------------------------------------------
|
|
198
|
-
// prepare_pdf_save / commit_pdf_save (§10.1/§10.3 two-phase save)
|
|
199
|
-
// -------------------------------------------------------------------------------------------
|
|
200
|
-
/** Mirrors `src-tauri/crates/pdf/src/save.rs`'s `select_compose_base` exactly: a `<=1`-byte
|
|
201
|
-
* annotation payload is the frontend's "no annotation edits" sentinel (base = source); a real
|
|
202
|
-
* pdf.js incremental update is the original bytes verbatim plus appended annotation objects/xref
|
|
203
|
-
* (base = those bytes); anything else is rejected rather than silently guessed at. */
|
|
204
|
-
function selectComposeBase(source, annotationPdf) {
|
|
205
|
-
if (annotationPdf.length <= 1)
|
|
206
|
-
return source;
|
|
207
|
-
if (annotationPdf.length > source.length && startsWithBytes(annotationPdf, source)) {
|
|
208
|
-
return annotationPdf;
|
|
209
|
-
}
|
|
210
|
-
throw {
|
|
211
|
-
code: "annotation_serialization_failed",
|
|
212
|
-
message: "annotations could not be serialized",
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
const qaSaveTokens = new Map();
|
|
216
|
-
export async function qaPreparePdfSave(request) {
|
|
217
|
-
// Matches `pdf_adapter.rs`'s own explicit empty check, which runs before `select_compose_base`
|
|
218
|
-
// even sees the bytes (its own `<=1` branch would otherwise also accept a genuinely-empty
|
|
219
|
-
// payload as "no annotation edits", which is not what an empty/failed serialization means).
|
|
220
|
-
if (request.annotationPdfBytes.length === 0) {
|
|
221
|
-
throw {
|
|
222
|
-
code: "annotation_serialization_failed",
|
|
223
|
-
message: "annotations could not be serialized",
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
const source = await currentDiskBytes(request.sourcePath);
|
|
227
|
-
if (!source) {
|
|
228
|
-
throw { code: "malformed_content", message: `qa disk has no PDF at ${request.sourcePath}` };
|
|
229
|
-
}
|
|
230
|
-
const actualSha = await sha256Hex(source);
|
|
231
|
-
if (request.expectedSourceSha256 !== actualSha) {
|
|
232
|
-
throw { code: "stale_source", message: "PDF changed since edit started" };
|
|
233
|
-
}
|
|
234
|
-
const candidateBytes = selectComposeBase(source, request.annotationPdfBytes);
|
|
235
|
-
const candidateSha256 = await sha256Hex(candidateBytes);
|
|
236
|
-
const token = crypto.randomUUID();
|
|
237
|
-
qaSaveTokens.set(token, {
|
|
238
|
-
sourcePath: request.sourcePath,
|
|
239
|
-
sourceSha256: actualSha,
|
|
240
|
-
candidateBytes,
|
|
241
|
-
});
|
|
242
|
-
return {
|
|
243
|
-
token,
|
|
244
|
-
candidateSha256,
|
|
245
|
-
candidateBytes,
|
|
246
|
-
affectedPages: affectedPagesFor(request.sourceCommands),
|
|
247
|
-
expectedText: [],
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
export async function qaCommitPdfSave(token, targetPath) {
|
|
251
|
-
// Single-use regardless of outcome — mirrors `PdfSaveTokenStore::consume`'s "removes the token
|
|
252
|
-
// record on the very first commit_pdf_save call for it, success or failure."
|
|
253
|
-
const record = qaSaveTokens.get(token);
|
|
254
|
-
qaSaveTokens.delete(token);
|
|
255
|
-
if (!record) {
|
|
256
|
-
throw { code: "write_conflict", message: "save token invalid or already used" };
|
|
257
|
-
}
|
|
258
|
-
const target = targetPath ?? record.sourcePath;
|
|
259
|
-
if (target === record.sourcePath) {
|
|
260
|
-
// Same-path Save rechecks source sha immediately before the "atomic" write, same as
|
|
261
|
-
// `resolve_commit`'s same-path recheck.
|
|
262
|
-
const current = await currentDiskBytes(record.sourcePath);
|
|
263
|
-
const currentSha = current ? await sha256Hex(current) : "";
|
|
264
|
-
if (currentSha !== record.sourceSha256) {
|
|
265
|
-
throw { code: "write_conflict", message: "target changed before save" };
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
qaDisk.set(target, record.candidateBytes);
|
|
269
|
-
const committedSha256 = await sha256Hex(record.candidateBytes);
|
|
270
|
-
return {
|
|
271
|
-
committedPath: target,
|
|
272
|
-
committedSha256,
|
|
273
|
-
affectedPages: [],
|
|
274
|
-
revision: { documentSha256: committedSha256, pageCount: 0, pages: [] },
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
let registered = false;
|
|
278
|
-
/** Registers `window.__viewrightQa` once. Callers (`tauriBridge.ts`) must gate this behind their
|
|
279
|
-
* own `isBrowserQaMode()` check — kept unconditional here (idempotent via `registered`) rather
|
|
280
|
-
* than duplicating that check locally, since importing `isBrowserQaMode` from `tauriBridge.ts`
|
|
281
|
-
* would cycle back into this module's own `./browserQa` import. */
|
|
282
|
-
export function registerViewrightQaGlobal() {
|
|
283
|
-
if (registered || typeof window === "undefined")
|
|
284
|
-
return;
|
|
285
|
-
registered = true;
|
|
286
|
-
window.__viewrightQa = {
|
|
287
|
-
pdfDiskBase64(path) {
|
|
288
|
-
const bytes = qaDisk.get(path);
|
|
289
|
-
return bytes ? bytesToBase64(bytes) : null;
|
|
290
|
-
},
|
|
291
|
-
pdfDiskLatin1(path) {
|
|
292
|
-
const bytes = qaDisk.get(path);
|
|
293
|
-
return bytes ? bytesToBinaryString(bytes) : null;
|
|
294
|
-
},
|
|
295
|
-
async originalPdfDiskLatin1(path) {
|
|
296
|
-
const doc = await qaDocumentForPath(path);
|
|
297
|
-
if (!doc?.assetUrl)
|
|
298
|
-
return null;
|
|
299
|
-
return bytesToBinaryString(dataUrlToBytes(doc.assetUrl));
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { LoadedDocument } from "../documents/types.js";
|
|
2
|
-
export declare function readQaTextDocument(doc: LoadedDocument): Promise<LoadedDocument>;
|
|
3
|
-
export declare function writeQaTextDocument(path: string, content: string, encoding?: string, expected?: string): Promise<{
|
|
4
|
-
encoding: string;
|
|
5
|
-
sourceSha256: string;
|
|
6
|
-
}>;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// IPC-boundary disk for browser journeys. Session storage belongs to disposable QA
|
|
2
|
-
// browser profile; survives reload, never reads/writes native app data or user files.
|
|
3
|
-
const PREFIX = "viewright.qa.text-disk:";
|
|
4
|
-
function diskKey(path) {
|
|
5
|
-
return (PREFIX +
|
|
6
|
-
(new URLSearchParams(location.search).get("qaQuality") === "1" ? "quality:" : "default:") +
|
|
7
|
-
path);
|
|
8
|
-
}
|
|
9
|
-
function read(path) {
|
|
10
|
-
const raw = sessionStorage.getItem(diskKey(path));
|
|
11
|
-
return raw ? JSON.parse(raw) : null;
|
|
12
|
-
}
|
|
13
|
-
async function digest(content) {
|
|
14
|
-
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
|
|
15
|
-
return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
16
|
-
}
|
|
17
|
-
function publishReadback() {
|
|
18
|
-
window.__viewrightQaText = { read };
|
|
19
|
-
}
|
|
20
|
-
export async function readQaTextDocument(doc) {
|
|
21
|
-
if (!["txt", "md", "mdx", "html"].includes(doc.format))
|
|
22
|
-
return doc;
|
|
23
|
-
const content = doc.format === "md" && new URLSearchParams(location.search).get("qaQuality") === "1"
|
|
24
|
-
? doc.content + "\n\n protected code\n second line\n"
|
|
25
|
-
: doc.content;
|
|
26
|
-
const entry = read(doc.path) ?? {
|
|
27
|
-
content,
|
|
28
|
-
encoding: doc.encoding ?? "UTF-8",
|
|
29
|
-
sourceSha256: await digest(content),
|
|
30
|
-
};
|
|
31
|
-
sessionStorage.setItem(diskKey(doc.path), JSON.stringify(entry));
|
|
32
|
-
publishReadback();
|
|
33
|
-
return {
|
|
34
|
-
...doc,
|
|
35
|
-
...entry,
|
|
36
|
-
fingerprint: { mtimeMs: 1, size: new TextEncoder().encode(entry.content).length },
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export async function writeQaTextDocument(path, content, encoding = "UTF-8", expected) {
|
|
40
|
-
const previous = read(path);
|
|
41
|
-
if (expected && previous && expected !== previous.sourceSha256) {
|
|
42
|
-
throw new Error("QA disk changed before save");
|
|
43
|
-
}
|
|
44
|
-
const entry = { content, encoding, sourceSha256: await digest(content) };
|
|
45
|
-
sessionStorage.setItem(diskKey(path), JSON.stringify(entry));
|
|
46
|
-
publishReadback();
|
|
47
|
-
return { encoding, sourceSha256: entry.sourceSha256 };
|
|
48
|
-
}
|