@cs-object/cli 0.1.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 +202 -0
- package/NOTICE +4 -0
- package/README.md +195 -0
- package/dist/chunk-7XCJ6AF3.js +607 -0
- package/dist/chunk-KYWIVB2V.js +341 -0
- package/dist/chunk-MOM6Q2VG.js +44 -0
- package/dist/chunk-OYQDZWT7.js +145 -0
- package/dist/chunk-UCPKZ67Z.js +88 -0
- package/dist/cli.js +262 -0
- package/dist/dev-server-BVY4Q6R6.js +732 -0
- package/dist/development.js +166 -0
- package/dist/html-AAIMIB2S.js +9 -0
- package/dist/pdf-WXWGZ6NC.js +9 -0
- package/package.json +60 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseStrictJson
|
|
3
|
+
} from "./chunk-KYWIVB2V.js";
|
|
4
|
+
import {
|
|
5
|
+
stringifyJson
|
|
6
|
+
} from "./chunk-MOM6Q2VG.js";
|
|
7
|
+
|
|
8
|
+
// src/evidence.ts
|
|
9
|
+
import {
|
|
10
|
+
mkdirSync,
|
|
11
|
+
mkdtempSync,
|
|
12
|
+
readFileSync as readFileSync2,
|
|
13
|
+
readdirSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
lstatSync
|
|
18
|
+
} from "fs";
|
|
19
|
+
import { basename, dirname as dirname2, join } from "path";
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
import { isDeepStrictEqual } from "util";
|
|
22
|
+
import {
|
|
23
|
+
BoundPreparedDocumentSchema
|
|
24
|
+
} from "@cs-object/core";
|
|
25
|
+
|
|
26
|
+
// src/assets.ts
|
|
27
|
+
import { readFileSync, realpathSync, statSync } from "fs";
|
|
28
|
+
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
import {
|
|
31
|
+
ResolvedAssetSchema
|
|
32
|
+
} from "@cs-object/core";
|
|
33
|
+
import { DocumentPreparationError } from "@cs-object/react";
|
|
34
|
+
var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
35
|
+
function captureAssets(sourcePath, execution) {
|
|
36
|
+
const root = dirname(realpathSync(sourcePath));
|
|
37
|
+
const captures = /* @__PURE__ */ new Map();
|
|
38
|
+
return execution.assets.map((asset) => {
|
|
39
|
+
try {
|
|
40
|
+
const path = realpathSync(
|
|
41
|
+
resolve(root, dirname(asset.moduleId), asset.path)
|
|
42
|
+
);
|
|
43
|
+
const within = relative(root, path);
|
|
44
|
+
if (within === ".." || within.startsWith("../") || isAbsolute(within))
|
|
45
|
+
throw new Error("Asset resolves outside the entry source directory");
|
|
46
|
+
if (!statSync(path).isFile())
|
|
47
|
+
throw new Error("Asset is not a regular file");
|
|
48
|
+
let bytes = captures.get(path);
|
|
49
|
+
if (!bytes) {
|
|
50
|
+
bytes = readFileSync(path);
|
|
51
|
+
captures.set(path, bytes);
|
|
52
|
+
}
|
|
53
|
+
if (sha256(bytes) !== asset.sha256)
|
|
54
|
+
throw new Error("Asset bytes changed since Python capture");
|
|
55
|
+
const png = bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
56
|
+
const jpeg = bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
|
|
57
|
+
if (asset.mediaType === "image/png" && !png)
|
|
58
|
+
throw new Error("Declared PNG has the wrong media signature");
|
|
59
|
+
if (asset.mediaType === "image/jpeg" && !jpeg)
|
|
60
|
+
throw new Error("Declared JPEG has the wrong media signature");
|
|
61
|
+
if (asset.mediaType === "image/svg+xml") {
|
|
62
|
+
const xml = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
63
|
+
if (png || jpeg || /<!DOCTYPE|<!ENTITY/i.test(xml) || !/^\s*(?:<\?xml[^?]*\?>\s*)?<svg[\s>]/i.test(xml))
|
|
64
|
+
throw new Error(
|
|
65
|
+
"SVG requires UTF-8 SVG markup without document types or entities"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return ResolvedAssetSchema.parse({
|
|
69
|
+
asset,
|
|
70
|
+
dataUrl: `data:${asset.mediaType};base64,${bytes.toString("base64")}`
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new DocumentPreparationError([
|
|
74
|
+
{
|
|
75
|
+
code: "ASSET_CAPTURE_FAILED",
|
|
76
|
+
stage: "document",
|
|
77
|
+
message: `${asset.moduleId}:${asset.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
78
|
+
}
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/evidence.ts
|
|
85
|
+
var jsonBytes = (value) => Buffer.from(`${stringifyJson(value)}
|
|
86
|
+
`);
|
|
87
|
+
var pointerKey = (key) => key.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
88
|
+
var suppliedJson = (value) => Array.isArray(value) ? value.map(suppliedJson) : value !== null && typeof value === "object" ? Object.fromEntries(
|
|
89
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, suppliedJson(entry)])
|
|
90
|
+
) : value;
|
|
91
|
+
function fields(value, artifact, pointer = "") {
|
|
92
|
+
if (value !== null && typeof value === "object" && Object.keys(value).length)
|
|
93
|
+
return Object.entries(value).flatMap(
|
|
94
|
+
([key, entry]) => fields(entry, artifact, `${pointer}/${pointerKey(key)}`)
|
|
95
|
+
);
|
|
96
|
+
return [{ artifact, pointer, sha256: sha256(jsonBytes(value)) }];
|
|
97
|
+
}
|
|
98
|
+
function retainSource(capture, document2) {
|
|
99
|
+
BoundPreparedDocumentSchema.parse({ execution: capture.execution, document: document2 });
|
|
100
|
+
const preparedBytes = jsonBytes(document2);
|
|
101
|
+
const roundTrip = parseStrictJson(preparedBytes.toString("utf8"));
|
|
102
|
+
if (!isDeepStrictEqual(roundTrip, suppliedJson(document2)))
|
|
103
|
+
throw new Error("Prepared data did not survive exact JSON round trip");
|
|
104
|
+
const execution = parseStrictJson(
|
|
105
|
+
new TextDecoder("utf-8", { fatal: true }).decode(capture.executionBytes)
|
|
106
|
+
);
|
|
107
|
+
const payloads = [
|
|
108
|
+
{ path: "execution.json", bytes: capture.executionBytes },
|
|
109
|
+
{ path: "verification.json", bytes: jsonBytes(capture.verification) },
|
|
110
|
+
{ path: "prepared.json", bytes: preparedBytes }
|
|
111
|
+
];
|
|
112
|
+
if (capture.referenceBytes)
|
|
113
|
+
payloads.push({ path: "reference.json", bytes: capture.referenceBytes });
|
|
114
|
+
return {
|
|
115
|
+
payloads,
|
|
116
|
+
retainedFields: [
|
|
117
|
+
...fields(execution, "execution.json"),
|
|
118
|
+
...fields(roundTrip, "prepared.json")
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function publishDocumentEvidence(options) {
|
|
123
|
+
const { outPath, artifact } = options;
|
|
124
|
+
const bytes = artifact.bytes;
|
|
125
|
+
mkdirSync(dirname2(outPath), { recursive: true });
|
|
126
|
+
const temporaryOutput = join(
|
|
127
|
+
dirname2(outPath),
|
|
128
|
+
`.${basename(outPath)}.${randomUUID()}.tmp`
|
|
129
|
+
);
|
|
130
|
+
try {
|
|
131
|
+
writeFileSync(temporaryOutput, bytes, { flag: "wx" });
|
|
132
|
+
if (!readFileSync2(temporaryOutput).equals(bytes)) {
|
|
133
|
+
throw new Error("Temporary document byte verification failed");
|
|
134
|
+
}
|
|
135
|
+
const evidence = options.retainEvidence === false ? void 0 : publishEvidenceBundle(options);
|
|
136
|
+
renameSync(temporaryOutput, outPath);
|
|
137
|
+
return evidence;
|
|
138
|
+
} finally {
|
|
139
|
+
try {
|
|
140
|
+
rmSync(temporaryOutput, { force: true });
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function publishEvidenceBundle(options) {
|
|
146
|
+
const { outPath, artifact, reportBytes, retained, presentation } = options;
|
|
147
|
+
const payloads = [
|
|
148
|
+
...retained.payloads,
|
|
149
|
+
{
|
|
150
|
+
path: "source-audit.json",
|
|
151
|
+
bytes: jsonBytes({
|
|
152
|
+
auditVersion: "1",
|
|
153
|
+
defaultDisposition: "Retained in the named evidence artifact; only explicit engineering projections are presented.",
|
|
154
|
+
retainedFields: retained.retainedFields,
|
|
155
|
+
presentationArtifact: "prepared.json",
|
|
156
|
+
presentation
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
];
|
|
160
|
+
const manifest = jsonBytes({
|
|
161
|
+
manifestVersion: "1",
|
|
162
|
+
[artifact.kind]: { path: outPath, sha256: sha256(artifact.bytes) },
|
|
163
|
+
prospectiveCommandReport: {
|
|
164
|
+
sha256: sha256(reportBytes),
|
|
165
|
+
publication: "Requires the actual successful stdout report and matching document; this manifest alone is not a publication receipt."
|
|
166
|
+
},
|
|
167
|
+
artifacts: payloads.map(({ path, bytes }) => ({
|
|
168
|
+
path,
|
|
169
|
+
sha256: sha256(bytes)
|
|
170
|
+
})),
|
|
171
|
+
outcomes: {
|
|
172
|
+
dataRetention: {
|
|
173
|
+
status: "passed",
|
|
174
|
+
fields: retained.retainedFields.length
|
|
175
|
+
},
|
|
176
|
+
engineeringPresentation: {
|
|
177
|
+
automatic: options.presentationCheck,
|
|
178
|
+
visualInspection: "pending"
|
|
179
|
+
},
|
|
180
|
+
sourceToDocumentConsistency: {
|
|
181
|
+
artifact: "verification.json",
|
|
182
|
+
pointer: "/checks/sourceToDocumentConsistency"
|
|
183
|
+
},
|
|
184
|
+
independentReferenceAgreement: {
|
|
185
|
+
artifact: "verification.json",
|
|
186
|
+
pointer: "/checks/independentReferenceAgreement"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const hash = sha256(manifest);
|
|
191
|
+
const parent = `${outPath}.evidence`;
|
|
192
|
+
const destination = join(parent, hash);
|
|
193
|
+
let temporaryEvidence;
|
|
194
|
+
try {
|
|
195
|
+
mkdirSync(parent, { recursive: true });
|
|
196
|
+
temporaryEvidence = mkdtempSync(join(parent, ".tmp-"));
|
|
197
|
+
const files = [...payloads, { path: "manifest.json", bytes: manifest }];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
const path = join(temporaryEvidence, file.path);
|
|
200
|
+
writeFileSync(path, file.bytes, { flag: "wx" });
|
|
201
|
+
if (!readFileSync2(path).equals(file.bytes))
|
|
202
|
+
throw new Error(`Evidence verification failed: ${file.path}`);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
renameSync(temporaryEvidence, destination);
|
|
206
|
+
temporaryEvidence = void 0;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (!lstatSync(destination).isDirectory() || readdirSync(destination).sort().join("\0") !== files.map((file) => file.path).sort().join("\0") || files.some(
|
|
209
|
+
(file) => !lstatSync(join(destination, file.path)).isFile() || !readFileSync2(join(destination, file.path)).equals(file.bytes)
|
|
210
|
+
))
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
if (temporaryEvidence) {
|
|
214
|
+
rmSync(temporaryEvidence, { recursive: true });
|
|
215
|
+
temporaryEvidence = void 0;
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
if (temporaryEvidence) {
|
|
219
|
+
try {
|
|
220
|
+
rmSync(temporaryEvidence, { recursive: true, force: true });
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { path: join(destination, "manifest.json"), sha256: hash };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/prepared-html.ts
|
|
229
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
230
|
+
import { createRequire } from "module";
|
|
231
|
+
import { createElement } from "react";
|
|
232
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
233
|
+
import { PreparedFormulaSheet } from "@cs-object/react";
|
|
234
|
+
function buildPreparedHtml(preparedDocument, reviewNotice) {
|
|
235
|
+
const css = readFileSync3(
|
|
236
|
+
createRequire(import.meta.url).resolve("@cs-object/react/style.css"),
|
|
237
|
+
"utf8"
|
|
238
|
+
);
|
|
239
|
+
const markup = renderToStaticMarkup(
|
|
240
|
+
createElement(PreparedFormulaSheet, { document: preparedDocument })
|
|
241
|
+
);
|
|
242
|
+
const notice = reviewNotice ? renderToStaticMarkup(
|
|
243
|
+
createElement(
|
|
244
|
+
"p",
|
|
245
|
+
{
|
|
246
|
+
"data-cso-review-notice": true,
|
|
247
|
+
style: {
|
|
248
|
+
fontSize: "10pt",
|
|
249
|
+
margin: "0 0 4mm",
|
|
250
|
+
padding: "2mm",
|
|
251
|
+
border: "1px solid #bbb"
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
reviewNotice
|
|
255
|
+
)
|
|
256
|
+
) : "";
|
|
257
|
+
return [
|
|
258
|
+
'<!doctype html><html lang="en"><head><meta charset="utf-8">',
|
|
259
|
+
renderToStaticMarkup(createElement("title", null, preparedDocument.title)),
|
|
260
|
+
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:">`,
|
|
261
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1"><style>${css}</style></head><body class="formula-sheet-printing cso-standalone-report">`,
|
|
262
|
+
`<div data-formula-sheet-print-root="true">${notice}${markup}</div></body></html>`
|
|
263
|
+
].join("");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/pdf-rendering.ts
|
|
267
|
+
import { chromium } from "playwright";
|
|
268
|
+
var LayoutInspectionError = class extends Error {
|
|
269
|
+
constructor(diagnostics) {
|
|
270
|
+
super(diagnostics.map((diagnostic) => diagnostic.message).join("\n"));
|
|
271
|
+
this.diagnostics = diagnostics;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
function assertPassiveSvgAssetsInPage(assets) {
|
|
275
|
+
for (const resolved of assets) {
|
|
276
|
+
if (resolved.asset.mediaType !== "image/svg+xml") continue;
|
|
277
|
+
const xml = new TextDecoder("utf-8", { fatal: true }).decode(
|
|
278
|
+
Uint8Array.from(
|
|
279
|
+
atob(resolved.dataUrl.split(",")[1]),
|
|
280
|
+
(c) => c.charCodeAt(0)
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
const parsed = new DOMParser().parseFromString(xml, "image/svg+xml");
|
|
284
|
+
if (parsed.querySelector("parsererror") || parsed.documentElement.localName !== "svg" || parsed.documentElement.namespaceURI !== "http://www.w3.org/2000/svg")
|
|
285
|
+
throw new Error("Invalid SVG XML");
|
|
286
|
+
const forbidden = /* @__PURE__ */ new Set([
|
|
287
|
+
"script",
|
|
288
|
+
"foreignobject",
|
|
289
|
+
"iframe",
|
|
290
|
+
"object",
|
|
291
|
+
"embed",
|
|
292
|
+
"audio",
|
|
293
|
+
"video",
|
|
294
|
+
"a",
|
|
295
|
+
"animate",
|
|
296
|
+
"animatetransform",
|
|
297
|
+
"animatemotion",
|
|
298
|
+
"discard",
|
|
299
|
+
"set",
|
|
300
|
+
"style"
|
|
301
|
+
]);
|
|
302
|
+
for (const element of parsed.querySelectorAll("*")) {
|
|
303
|
+
if (element.namespaceURI !== "http://www.w3.org/2000/svg" || forbidden.has(element.localName.toLowerCase()))
|
|
304
|
+
throw new Error(`Active SVG element ${element.localName}`);
|
|
305
|
+
for (const attr of element.attributes) {
|
|
306
|
+
const name = attr.localName.toLowerCase();
|
|
307
|
+
if (name.startsWith("on") || name === "style" || name === "base" || (name === "href" || name === "src") && !/^#[^\s]*$/.test(attr.value) || /url\s*\(/i.test(attr.value) && !/^url\(\s*#[A-Za-z_][\w.-]*\s*\)$/.test(attr.value))
|
|
308
|
+
throw new Error(`Active or external SVG attribute ${attr.name}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function waitForPrintAssetsInPage() {
|
|
314
|
+
let timer;
|
|
315
|
+
try {
|
|
316
|
+
await Promise.race([
|
|
317
|
+
Promise.all([
|
|
318
|
+
document.fonts.ready,
|
|
319
|
+
...Array.from(document.images, async (image) => {
|
|
320
|
+
await image.decode();
|
|
321
|
+
if (!image.naturalWidth || !image.naturalHeight)
|
|
322
|
+
throw new Error("Image decoded without dimensions");
|
|
323
|
+
})
|
|
324
|
+
]),
|
|
325
|
+
new Promise((_, reject) => {
|
|
326
|
+
timer = setTimeout(
|
|
327
|
+
() => reject(new Error("Image/font readiness timed out")),
|
|
328
|
+
15e3
|
|
329
|
+
);
|
|
330
|
+
})
|
|
331
|
+
]);
|
|
332
|
+
} finally {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function presentationInput(preparedDocument) {
|
|
337
|
+
return {
|
|
338
|
+
sections: preparedDocument.sections.map((section) => ({
|
|
339
|
+
id: section.id,
|
|
340
|
+
title: section.title,
|
|
341
|
+
sourcePlacementId: section.sourcePlacementId,
|
|
342
|
+
items: section.items.map((item) => ({
|
|
343
|
+
kind: item.kind,
|
|
344
|
+
sourcePlacementId: item.sourcePlacementId,
|
|
345
|
+
text: item.kind === "text" ? item.text.content : void 0,
|
|
346
|
+
figure: item.kind === "figure" ? item.figure : void 0
|
|
347
|
+
}))
|
|
348
|
+
})),
|
|
349
|
+
assets: preparedDocument.assets.map(({ asset }) => asset)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function inspectPreparedPresentationInPage(prepared) {
|
|
353
|
+
const mappings = [
|
|
354
|
+
{ pointer: "/title", role: "document title", selector: "h1" }
|
|
355
|
+
];
|
|
356
|
+
const placements = Array.from(
|
|
357
|
+
document.querySelectorAll("[data-source-placement]")
|
|
358
|
+
);
|
|
359
|
+
function mapItemPlacement(item, itemPointer) {
|
|
360
|
+
const element = placements.find(
|
|
361
|
+
(element2) => element2.dataset.sourcePlacement === item.sourcePlacementId
|
|
362
|
+
);
|
|
363
|
+
if (!element)
|
|
364
|
+
throw new Error(`Missing ordered placement ${item.sourcePlacementId}`);
|
|
365
|
+
const itemSelector = `[data-source-placement=${JSON.stringify(item.sourcePlacementId)}]`;
|
|
366
|
+
switch (item.kind) {
|
|
367
|
+
case "text":
|
|
368
|
+
if (!element.textContent?.includes(item.text ?? ""))
|
|
369
|
+
throw new Error("Missing authored text");
|
|
370
|
+
mappings.push({
|
|
371
|
+
pointer: `${itemPointer}/text/content`,
|
|
372
|
+
role: "authored text",
|
|
373
|
+
selector: itemSelector
|
|
374
|
+
});
|
|
375
|
+
break;
|
|
376
|
+
case "figure":
|
|
377
|
+
if (element.querySelector("img")?.alt !== item.figure?.alt || element.dataset.assetSha256 !== prepared.assets.find((asset) => asset.id === item.figure?.assetId)?.sha256)
|
|
378
|
+
throw new Error("Figure identity mismatch");
|
|
379
|
+
mappings.push({
|
|
380
|
+
pointer: `${itemPointer}/figure`,
|
|
381
|
+
role: "figure with caption and alt text",
|
|
382
|
+
selector: itemSelector
|
|
383
|
+
});
|
|
384
|
+
break;
|
|
385
|
+
case "symbol":
|
|
386
|
+
case "symbolRef":
|
|
387
|
+
if (!element.querySelector("math"))
|
|
388
|
+
throw new Error("Missing mathematical notation");
|
|
389
|
+
mappings.push({
|
|
390
|
+
pointer: item.kind === "symbol" ? `${itemPointer}/symbol` : `${itemPointer}/id`,
|
|
391
|
+
role: "engineering row projection: glyph, description, units, active formula, substitution, result and comment; internal fields remain evidence",
|
|
392
|
+
selector: itemSelector
|
|
393
|
+
});
|
|
394
|
+
break;
|
|
395
|
+
case "section":
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const [sectionIndex, section] of prepared.sections.entries()) {
|
|
400
|
+
const pointer = `/sections/${sectionIndex}`;
|
|
401
|
+
const heading = placements.find(
|
|
402
|
+
(element) => element.dataset.sourcePlacement === section.sourcePlacementId
|
|
403
|
+
);
|
|
404
|
+
if (!heading || heading.textContent !== section.title)
|
|
405
|
+
throw new Error(`Missing section ${section.id}`);
|
|
406
|
+
const selector = `[data-source-placement=${JSON.stringify(section.sourcePlacementId)}]`;
|
|
407
|
+
mappings.push({
|
|
408
|
+
pointer: `${pointer}/title`,
|
|
409
|
+
role: "section title",
|
|
410
|
+
selector
|
|
411
|
+
});
|
|
412
|
+
for (const [itemIndex, item] of section.items.entries()) {
|
|
413
|
+
mapItemPlacement(item, `${pointer}/items/${itemIndex}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const element of document.querySelectorAll(
|
|
417
|
+
"[data-engineering-value]"
|
|
418
|
+
)) {
|
|
419
|
+
const pointer = element.dataset.engineeringValue;
|
|
420
|
+
if (pointer)
|
|
421
|
+
mappings.push({
|
|
422
|
+
pointer,
|
|
423
|
+
role: "selected engineering context",
|
|
424
|
+
selector: `[data-engineering-value=${JSON.stringify(pointer)}]`
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return mappings;
|
|
428
|
+
}
|
|
429
|
+
function inspectLayoutInPage(media) {
|
|
430
|
+
const sheet = document.querySelector("[data-formula-sheet]");
|
|
431
|
+
if (!sheet) throw new Error("Missing formula sheet");
|
|
432
|
+
const sheetBounds = sheet.getBoundingClientRect();
|
|
433
|
+
const findings = /* @__PURE__ */ new Map();
|
|
434
|
+
function recordOverflow({
|
|
435
|
+
element,
|
|
436
|
+
bounds,
|
|
437
|
+
containerLeft,
|
|
438
|
+
containerRight,
|
|
439
|
+
placement,
|
|
440
|
+
fallback,
|
|
441
|
+
message
|
|
442
|
+
}) {
|
|
443
|
+
if (!bounds.width || !bounds.height) return;
|
|
444
|
+
const overflowPx = Math.max(
|
|
445
|
+
0,
|
|
446
|
+
containerLeft - bounds.left,
|
|
447
|
+
bounds.right - containerRight
|
|
448
|
+
);
|
|
449
|
+
if (overflowPx <= 2) return;
|
|
450
|
+
const key = placement ?? fallback;
|
|
451
|
+
const previous = findings.get(key);
|
|
452
|
+
if (previous && (previous.layout?.overflowPx ?? 0) >= overflowPx) return;
|
|
453
|
+
const sourcePlacementId = placement?.getAttribute("data-source-placement") ?? void 0;
|
|
454
|
+
const selector = sourcePlacementId ? `[data-source-placement=${JSON.stringify(sourcePlacementId)}]` : "[data-formula-sheet]";
|
|
455
|
+
findings.set(key, {
|
|
456
|
+
code: "DOCUMENT_LAYOUT_OVERFLOW",
|
|
457
|
+
stage: "rendering",
|
|
458
|
+
check: "rendering",
|
|
459
|
+
message: `${message} in ${media} layout: ${selector}, ${element.tagName}, ${overflowPx.toFixed(2)}px beyond content bounds.`,
|
|
460
|
+
layout: {
|
|
461
|
+
media,
|
|
462
|
+
sourcePlacementId,
|
|
463
|
+
selector,
|
|
464
|
+
left: bounds.left,
|
|
465
|
+
right: bounds.right,
|
|
466
|
+
containerLeft,
|
|
467
|
+
containerRight,
|
|
468
|
+
overflowPx
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
for (const math of sheet.querySelectorAll("math")) {
|
|
473
|
+
const placement = math.closest("[data-source-placement]");
|
|
474
|
+
const row = math.closest(".cso-symbol-row") ?? sheet;
|
|
475
|
+
const cell = math.closest("td") ?? row;
|
|
476
|
+
const rowBounds = cell.getBoundingClientRect();
|
|
477
|
+
let containerLeft = Math.max(sheetBounds.left, rowBounds.left);
|
|
478
|
+
let containerRight = Math.min(sheetBounds.right, rowBounds.right);
|
|
479
|
+
const parent = math.closest("td") ?? math.parentElement;
|
|
480
|
+
const parentOverflow = parent && math.getBoundingClientRect().width > parent.getBoundingClientRect().width + 2;
|
|
481
|
+
if (parentOverflow && parent) {
|
|
482
|
+
const parentBounds = parent.getBoundingClientRect();
|
|
483
|
+
containerLeft = Math.max(containerLeft, parentBounds.left);
|
|
484
|
+
containerRight = Math.min(containerRight, parentBounds.right);
|
|
485
|
+
}
|
|
486
|
+
for (const element of [math, ...math.querySelectorAll("*")]) {
|
|
487
|
+
recordOverflow({
|
|
488
|
+
element,
|
|
489
|
+
bounds: element.getBoundingClientRect(),
|
|
490
|
+
containerLeft,
|
|
491
|
+
containerRight,
|
|
492
|
+
placement,
|
|
493
|
+
fallback: row,
|
|
494
|
+
message: "Mathematical notation overflows its cell or sheet"
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const element of sheet.querySelectorAll("*")) {
|
|
499
|
+
if (element.closest("math") || element.closest("svg")) continue;
|
|
500
|
+
const rectangles = [element.getBoundingClientRect()];
|
|
501
|
+
for (const node of element.childNodes) {
|
|
502
|
+
if (node.nodeType !== Node.TEXT_NODE || !node.textContent?.trim())
|
|
503
|
+
continue;
|
|
504
|
+
const range = document.createRange();
|
|
505
|
+
range.selectNodeContents(node);
|
|
506
|
+
rectangles.push(...range.getClientRects());
|
|
507
|
+
}
|
|
508
|
+
for (const bounds of rectangles) {
|
|
509
|
+
recordOverflow({
|
|
510
|
+
element,
|
|
511
|
+
bounds,
|
|
512
|
+
containerLeft: sheetBounds.left,
|
|
513
|
+
containerRight: sheetBounds.right,
|
|
514
|
+
placement: element.closest("[data-source-placement]"),
|
|
515
|
+
fallback: sheet,
|
|
516
|
+
message: "Document content exceeds printable sheet width"
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return [...findings.values()];
|
|
521
|
+
}
|
|
522
|
+
async function withPreparedPage(document2, html, usePage, signal) {
|
|
523
|
+
const browser = await chromium.launch({ headless: true, timeout: 3e4 });
|
|
524
|
+
const abort = () => {
|
|
525
|
+
void browser.close().catch(() => {
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
529
|
+
try {
|
|
530
|
+
signal?.throwIfAborted();
|
|
531
|
+
const page = await browser.newPage();
|
|
532
|
+
page.setDefaultTimeout(15e3);
|
|
533
|
+
const requests = [];
|
|
534
|
+
await page.route("**/*", (route) => {
|
|
535
|
+
requests.push(route.request().url());
|
|
536
|
+
return route.abort();
|
|
537
|
+
});
|
|
538
|
+
await page.evaluate(assertPassiveSvgAssetsInPage, document2.assets);
|
|
539
|
+
await page.setContent(html, { waitUntil: "load", timeout: 15e3 });
|
|
540
|
+
const diagnostics = [];
|
|
541
|
+
for (const media of ["screen", "print"]) {
|
|
542
|
+
await page.emulateMedia({ media });
|
|
543
|
+
await page.evaluate(waitForPrintAssetsInPage);
|
|
544
|
+
diagnostics.push(...await page.evaluate(inspectLayoutInPage, media));
|
|
545
|
+
}
|
|
546
|
+
if (requests.length)
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Document attempted external requests: ${requests.join(", ")}`
|
|
549
|
+
);
|
|
550
|
+
if (diagnostics.length) throw new LayoutInspectionError(diagnostics);
|
|
551
|
+
const presentation = await page.evaluate(
|
|
552
|
+
inspectPreparedPresentationInPage,
|
|
553
|
+
presentationInput(document2)
|
|
554
|
+
);
|
|
555
|
+
return await usePage(page, presentation);
|
|
556
|
+
} finally {
|
|
557
|
+
signal?.removeEventListener("abort", abort);
|
|
558
|
+
await browser.close();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function inspectPreparedHtml(document2, html) {
|
|
562
|
+
return withPreparedPage(
|
|
563
|
+
document2,
|
|
564
|
+
html,
|
|
565
|
+
async (_page, presentation) => presentation
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
function renderPreparedPdf(document2, html = buildPreparedHtml(document2), signal) {
|
|
569
|
+
return withPreparedPage(
|
|
570
|
+
document2,
|
|
571
|
+
html,
|
|
572
|
+
async (page, presentation) => {
|
|
573
|
+
let printTimer;
|
|
574
|
+
try {
|
|
575
|
+
const pdf = await Promise.race([
|
|
576
|
+
page.pdf({
|
|
577
|
+
format: "A4",
|
|
578
|
+
printBackground: true,
|
|
579
|
+
margin: { top: "0", right: "0", bottom: "0", left: "0" }
|
|
580
|
+
}),
|
|
581
|
+
new Promise((_, reject) => {
|
|
582
|
+
printTimer = setTimeout(
|
|
583
|
+
() => reject(new Error("PDF printing timed out")),
|
|
584
|
+
3e4
|
|
585
|
+
);
|
|
586
|
+
})
|
|
587
|
+
]);
|
|
588
|
+
return { pdf, presentation };
|
|
589
|
+
} finally {
|
|
590
|
+
clearTimeout(printTimer);
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
signal
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export {
|
|
598
|
+
sha256,
|
|
599
|
+
captureAssets,
|
|
600
|
+
jsonBytes,
|
|
601
|
+
retainSource,
|
|
602
|
+
publishDocumentEvidence,
|
|
603
|
+
buildPreparedHtml,
|
|
604
|
+
LayoutInspectionError,
|
|
605
|
+
inspectPreparedHtml,
|
|
606
|
+
renderPreparedPdf
|
|
607
|
+
};
|