@brianmsm/quarto-bento 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +93 -2
- package/bin/qbento-npm.mjs +62 -0
- package/distribution/LICENSE +21 -0
- package/distribution/PACKAGING.md +59 -0
- package/distribution/bin/qbento +5 -0
- package/distribution/bin/qbento.cmd +4 -0
- package/distribution/manifest.json +720 -0
- package/distribution/runtime/LICENSE +21 -0
- package/distribution/runtime/_extensions/qbento/_extension.yml +24 -0
- package/distribution/runtime/_extensions/qbento/capture.lua +21 -0
- package/distribution/runtime/_extensions/qbento/guard.lua +7 -0
- package/distribution/runtime/_extensions/qbento/post.ts +2 -0
- package/distribution/runtime/_extensions/qbento/pre.ts +2 -0
- package/distribution/runtime/_extensions/qbento/producer-activation.js +129 -0
- package/distribution/runtime/src/bridge/browser.js +168 -0
- package/distribution/runtime/src/bridge/isolation.ts +55 -0
- package/distribution/runtime/src/browser/chromium.ts +218 -0
- package/distribution/runtime/src/cli.ts +232 -0
- package/distribution/runtime/src/core/agent.ts +546 -0
- package/distribution/runtime/src/core/annotations.ts +384 -0
- package/distribution/runtime/src/core/capture.ts +203 -0
- package/distribution/runtime/src/core/component-actions.ts +550 -0
- package/distribution/runtime/src/core/component-frames.ts +59 -0
- package/distribution/runtime/src/core/component-sync.ts +290 -0
- package/distribution/runtime/src/core/design-diagnostics.ts +78 -0
- package/distribution/runtime/src/core/design-registry.ts +287 -0
- package/distribution/runtime/src/core/graph-relations.ts +354 -0
- package/distribution/runtime/src/core/graph-scope.ts +63 -0
- package/distribution/runtime/src/core/graph-sync.ts +228 -0
- package/distribution/runtime/src/core/io.ts +27 -0
- package/distribution/runtime/src/core/kpi-appearance.ts +205 -0
- package/distribution/runtime/src/core/layouts.ts +245 -0
- package/distribution/runtime/src/core/project.ts +851 -0
- package/distribution/runtime/src/core/reconcile.ts +384 -0
- package/distribution/runtime/src/core/recovery.ts +1255 -0
- package/distribution/runtime/src/core/slide-actions.ts +158 -0
- package/distribution/runtime/src/core/source-scope.ts +50 -0
- package/distribution/runtime/src/core/sync.ts +2557 -0
- package/distribution/runtime/src/core/widgets.ts +300 -0
- package/distribution/runtime/src/integration/install.ts +98 -0
- package/distribution/runtime/src/integration/invocation.ts +100 -0
- package/distribution/runtime/src/integration/quarto.ts +457 -0
- package/distribution/runtime/src/render/assets.ts +66 -0
- package/distribution/runtime/src/render/capture.lua +8 -0
- package/distribution/runtime/src/render/chart.ts +165 -0
- package/distribution/runtime/src/render/components.ts +199 -0
- package/distribution/runtime/src/render/design.ts +820 -0
- package/distribution/runtime/src/render/diagnostics.ts +36 -0
- package/distribution/runtime/src/render/graphs.ts +747 -0
- package/distribution/runtime/src/render/html.ts +82 -0
- package/distribution/runtime/src/render/images.ts +268 -0
- package/distribution/runtime/src/render/inline.ts +259 -0
- package/distribution/runtime/src/render/kpi.ts +385 -0
- package/distribution/runtime/src/render/lists.ts +183 -0
- package/distribution/runtime/src/render/math.ts +413 -0
- package/distribution/runtime/src/render/measure.ts +185 -0
- package/distribution/runtime/src/render/native-layout.ts +174 -0
- package/distribution/runtime/src/render/producer-capture.ts +319 -0
- package/distribution/runtime/src/render/producer-environment.R +49 -0
- package/distribution/runtime/src/render/producer-environment.ts +96 -0
- package/distribution/runtime/src/render/producer-profile.R +91 -0
- package/distribution/runtime/src/render/producer-site-profile.R +18 -0
- package/distribution/runtime/src/render/project.ts +1806 -0
- package/distribution/runtime/src/render/quarto.ts +127 -0
- package/distribution/runtime/src/render/registry.ts +233 -0
- package/distribution/runtime/src/render/resources.ts +292 -0
- package/distribution/runtime/src/render/shell.ts +110 -0
- package/distribution/runtime/src/render/source-inline.ts +169 -0
- package/distribution/runtime/src/render/table.ts +220 -0
- package/distribution/runtime/src/render/text-overflow.ts +208 -0
- package/distribution/runtime/src/render/types.ts +71 -0
- package/distribution/runtime/src/render/validate.ts +572 -0
- package/distribution/runtime/src/rich/project.ts +117 -0
- package/distribution/runtime/src/rich/table.ts +374 -0
- package/distribution/runtime/src/source/annotations.ts +490 -0
- package/distribution/runtime/src/source/attributes.ts +415 -0
- package/distribution/runtime/src/source/chart.ts +165 -0
- package/distribution/runtime/src/source/code-language.ts +277 -0
- package/distribution/runtime/src/source/component-insertion.ts +160 -0
- package/distribution/runtime/src/source/deletion.ts +278 -0
- package/distribution/runtime/src/source/graphs.ts +517 -0
- package/distribution/runtime/src/source/identities.ts +256 -0
- package/distribution/runtime/src/source/identity-id.ts +6 -0
- package/distribution/runtime/src/source/image.ts +50 -0
- package/distribution/runtime/src/source/kpi.ts +196 -0
- package/distribution/runtime/src/source/mod.ts +876 -0
- package/distribution/runtime/src/source/notes.ts +308 -0
- package/distribution/runtime/src/source/structure.ts +647 -0
- package/distribution/runtime/src/source/table.ts +290 -0
- package/distribution/runtime/src/source/text-conversion.ts +146 -0
- package/distribution/runtime/src/state/components.ts +281 -0
- package/distribution/runtime/src/state/graphs.ts +200 -0
- package/distribution/runtime/src/state/json.ts +162 -0
- package/distribution/runtime/src/state/merge.ts +1034 -0
- package/distribution/runtime/src/state/mod.ts +5 -0
- package/distribution/runtime/src/state/store.ts +226 -0
- package/distribution/runtime/src/state/types.ts +151 -0
- package/distribution/runtime/src/studio/client.js +892 -0
- package/distribution/runtime/src/studio/components.js +188 -0
- package/distribution/runtime/src/studio/index.html +283 -0
- package/distribution/runtime/src/studio/layouts.js +170 -0
- package/distribution/runtime/src/studio/registry.js +283 -0
- package/distribution/runtime/src/studio/relations.js +169 -0
- package/distribution/runtime/src/studio/server.ts +461 -0
- package/distribution/runtime/src/widgets/deck.ts +39 -0
- package/distribution/runtime/src/widgets/plotly.ts +354 -0
- package/distribution/runtime/src/widgets/presenter.ts +367 -0
- package/distribution/runtime/src/widgets/snapshot.ts +116 -0
- package/distribution/runtime/vendor/bento/FONT_NOTICES.md +49 -0
- package/distribution/runtime/vendor/bento/LICENSE +21 -0
- package/distribution/runtime/vendor/bento/THIRD_PARTY_NOTICES.md +51 -0
- package/distribution/runtime/vendor/bento/font-licenses/fraunces-OFL.txt +93 -0
- package/distribution/runtime/vendor/bento/font-licenses/instrument-sans-OFL.txt +93 -0
- package/distribution/runtime/vendor/bento/pin.json +69 -0
- package/distribution/runtime/vendor/bento/provenance/slides.package-lock.json +1928 -0
- package/distribution/runtime/vendor/bento/provenance/slides.package.json +28 -0
- package/distribution/runtime/vendor/bento/runtime-dependencies.json +244 -0
- package/distribution/runtime/vendor/bento/runtime-licenses.json +4038 -0
- package/distribution/runtime/vendor/bento/shell.html +269 -0
- package/distribution/runtime/vendor/bento/slides.schema.json +2379 -0
- package/package.json +40 -4
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { producerOutputChildren } from "../render/producer-capture.ts";
|
|
2
|
+
/** Computed gt HTML tables only: preserve executed rich content as a static PNG. */
|
|
3
|
+
import { hashSource, scanSource, type SourceDocument } from "../source/mod.ts";
|
|
4
|
+
import { canonical } from "../state/json.ts";
|
|
5
|
+
import { tableProducer } from "../source/table.ts";
|
|
6
|
+
import type { Ast, Node } from "../render/types.ts";
|
|
7
|
+
import { withHeadlessChromium } from "../browser/chromium.ts";
|
|
8
|
+
export interface RichTableCandidate {
|
|
9
|
+
version: 1;
|
|
10
|
+
kind: "gt-static";
|
|
11
|
+
source: {
|
|
12
|
+
sourceHash: string;
|
|
13
|
+
sourceId: string;
|
|
14
|
+
producerId: string;
|
|
15
|
+
slideId: string;
|
|
16
|
+
chunkHash: string;
|
|
17
|
+
outputHash: string;
|
|
18
|
+
};
|
|
19
|
+
pandocApiVersion: number[];
|
|
20
|
+
blocks: Node[];
|
|
21
|
+
}
|
|
22
|
+
export interface RichTableReceipt {
|
|
23
|
+
version: 1;
|
|
24
|
+
kind: "gt-static-png";
|
|
25
|
+
source: RichTableCandidate["source"];
|
|
26
|
+
htmlHash: string;
|
|
27
|
+
pngHash: string;
|
|
28
|
+
textHash: string;
|
|
29
|
+
browser: string;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
pixelRatio: 2;
|
|
33
|
+
rows: number;
|
|
34
|
+
cells: number;
|
|
35
|
+
minFontSize: number;
|
|
36
|
+
minBodyFontSize: number;
|
|
37
|
+
}
|
|
38
|
+
export interface RichTableSnapshot {
|
|
39
|
+
candidate: RichTableCandidate;
|
|
40
|
+
png: Uint8Array;
|
|
41
|
+
receipt: RichTableReceipt;
|
|
42
|
+
diagnostic: { code: "STATIC_TABLE_PNG_FALLBACK"; message: string };
|
|
43
|
+
}
|
|
44
|
+
function fail(message: string): never {
|
|
45
|
+
throw new Error(`UNSUPPORTED_RICH_TABLE: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
function attr(node: Node): [string, string[], unknown[]] {
|
|
48
|
+
return (node.c as [[string, string[], unknown[]], unknown])[0];
|
|
49
|
+
}
|
|
50
|
+
function children(node: Node): Node[] {
|
|
51
|
+
return (node.c as [unknown, Node[]])[1];
|
|
52
|
+
}
|
|
53
|
+
function richOutput(contents: Node[]): Node[] | undefined {
|
|
54
|
+
if (
|
|
55
|
+
contents.length !== 1 || contents[0].t !== "Div" ||
|
|
56
|
+
canonical(attr(contents[0])) !== canonical(["", ["cell"], []])
|
|
57
|
+
) return;
|
|
58
|
+
const outputs = children(contents[0]);
|
|
59
|
+
if (
|
|
60
|
+
outputs.length !== 1 || outputs[0].t !== "Div" ||
|
|
61
|
+
canonical(attr(outputs[0])) !== canonical(["", ["cell-output-display"], []])
|
|
62
|
+
) return;
|
|
63
|
+
const output = children(outputs[0]);
|
|
64
|
+
// The ordinary extension captures before Quarto's final descaffold pass.
|
|
65
|
+
// Quarto 1.10.19 removes exactly this anonymous scaffold without presentation.
|
|
66
|
+
if (
|
|
67
|
+
output.length === 1 && output[0].t === "Div" &&
|
|
68
|
+
canonical(attr(output[0])) === canonical(["", ["quarto-scaffold"], []])
|
|
69
|
+
) {
|
|
70
|
+
return children(output[0]);
|
|
71
|
+
}
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
function looksLikeGt(value: unknown): boolean {
|
|
75
|
+
if (Array.isArray(value)) return value.some(looksLikeGt);
|
|
76
|
+
if (value && typeof value === "object") {
|
|
77
|
+
const n = value as Node;
|
|
78
|
+
if (n.t === "Table") {
|
|
79
|
+
return (n.c as unknown[][])?.[0]?.[1] instanceof Array &&
|
|
80
|
+
((n.c as unknown[][])[0][1] as string[]).includes("gt_table");
|
|
81
|
+
}
|
|
82
|
+
return looksLikeGt(n.c);
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
export function collectRichTableCandidates(
|
|
87
|
+
ast: Ast,
|
|
88
|
+
document: SourceDocument,
|
|
89
|
+
): RichTableCandidate[] {
|
|
90
|
+
if (hashSource(document.text) !== document.hash) {
|
|
91
|
+
fail("source snapshot hash is stale");
|
|
92
|
+
}
|
|
93
|
+
const source = scanSource(document.text);
|
|
94
|
+
if (source.diagnostics.length) {
|
|
95
|
+
fail("resolve original source mapping diagnostics first");
|
|
96
|
+
}
|
|
97
|
+
const api = (ast as unknown as Record<string, unknown>)["pandoc-api-version"];
|
|
98
|
+
const result: RichTableCandidate[] = [], visited = new Set<string>();
|
|
99
|
+
function visit(nodes: Node[]) {
|
|
100
|
+
for (const node of nodes) {
|
|
101
|
+
if (node.t !== "Div") continue;
|
|
102
|
+
const [id] = attr(node),
|
|
103
|
+
matches = source.blocks.filter((b) =>
|
|
104
|
+
b.id === id && b.kind === "div" && b.stable && b.explicitId
|
|
105
|
+
);
|
|
106
|
+
if (matches.length === 1 && looksLikeGt(children(node))) {
|
|
107
|
+
if (visited.has(id)) fail("duplicate executed wrapper identity");
|
|
108
|
+
visited.add(id);
|
|
109
|
+
if (
|
|
110
|
+
canonical(attr(node)) !== canonical([id, [], []]) ||
|
|
111
|
+
matches[0].raw.split(/\r?\n/, 1)[0].trim() !== `::: {#${id}}`
|
|
112
|
+
) {
|
|
113
|
+
fail(
|
|
114
|
+
"this slice requires a standalone wrapper declared exactly as ::: {#id}",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const producer = tableProducer(source, id),
|
|
118
|
+
blocks = richOutput(
|
|
119
|
+
producerOutputChildren(
|
|
120
|
+
ast,
|
|
121
|
+
children(node),
|
|
122
|
+
source.hash,
|
|
123
|
+
producer.id,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
if (
|
|
127
|
+
!blocks || blocks.length !== 3 || blocks[0].t !== "RawBlock" ||
|
|
128
|
+
blocks[1].t !== "Table" || blocks[2].t !== "RawBlock"
|
|
129
|
+
) {
|
|
130
|
+
fail(
|
|
131
|
+
"use exactly one gt table under one non-echoed producer cell; additional output cannot be dropped",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const open = blocks[0].c as [string, string],
|
|
135
|
+
close = blocks[2].c as [string, string];
|
|
136
|
+
if (
|
|
137
|
+
open[0] !== "html" || close[0] !== "html" ||
|
|
138
|
+
!/^\s*<div\b[^>]*>\s*<style>[\s\S]*<\/style>\s*$/.test(open[1]) ||
|
|
139
|
+
!/^\s*<\/div>\s*$/.test(close[1])
|
|
140
|
+
) fail("gt output needs one original scoped CSS container and table");
|
|
141
|
+
if (
|
|
142
|
+
!Array.isArray(api) || !api.length || api.length > 8 ||
|
|
143
|
+
api.some((v) => !Number.isSafeInteger(v) || Number(v) < 0)
|
|
144
|
+
) fail("captured Pandoc API version is required");
|
|
145
|
+
if (
|
|
146
|
+
!matches[0].slideId ||
|
|
147
|
+
!source.slides.some((s) => s.id === matches[0].slideId && s.stable)
|
|
148
|
+
) fail("use an explicit stable slide for the table");
|
|
149
|
+
if (canonical(blocks).length > 2_000_000) {
|
|
150
|
+
fail("table output exceeds 2 MB");
|
|
151
|
+
}
|
|
152
|
+
result.push({
|
|
153
|
+
version: 1,
|
|
154
|
+
kind: "gt-static",
|
|
155
|
+
source: {
|
|
156
|
+
sourceHash: source.hash,
|
|
157
|
+
sourceId: id,
|
|
158
|
+
producerId: producer.id,
|
|
159
|
+
slideId: matches[0].slideId,
|
|
160
|
+
chunkHash: hashSource(producer.raw),
|
|
161
|
+
outputHash: hashSource(canonical(blocks)),
|
|
162
|
+
},
|
|
163
|
+
pandocApiVersion: api as number[],
|
|
164
|
+
blocks: structuredClone(blocks),
|
|
165
|
+
});
|
|
166
|
+
} else visit(children(node));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
visit(ast.blocks);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
export async function richTableHtml(
|
|
173
|
+
candidate: RichTableCandidate,
|
|
174
|
+
): Promise<string> {
|
|
175
|
+
if (
|
|
176
|
+
candidate.version !== 1 || candidate.kind !== "gt-static" ||
|
|
177
|
+
hashSource(canonical(candidate.blocks)) !== candidate.source.outputHash
|
|
178
|
+
) fail("candidate output receipt mismatch");
|
|
179
|
+
const command = new Deno.Command("quarto", {
|
|
180
|
+
args: ["pandoc", "--from=json", "--to=html5", "--wrap=none"],
|
|
181
|
+
stdin: "piped",
|
|
182
|
+
stdout: "piped",
|
|
183
|
+
stderr: "piped",
|
|
184
|
+
}).spawn();
|
|
185
|
+
const writer = command.stdin.getWriter();
|
|
186
|
+
await writer.write(
|
|
187
|
+
new TextEncoder().encode(
|
|
188
|
+
JSON.stringify({
|
|
189
|
+
"pandoc-api-version": candidate.pandocApiVersion,
|
|
190
|
+
meta: {},
|
|
191
|
+
blocks: candidate.blocks,
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
await writer.close();
|
|
196
|
+
const output = await command.output();
|
|
197
|
+
if (!output.success) {
|
|
198
|
+
fail(
|
|
199
|
+
`fragment HTML writer failed: ${
|
|
200
|
+
new TextDecoder().decode(output.stderr).slice(-1000)
|
|
201
|
+
}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return new TextDecoder().decode(output.stdout);
|
|
205
|
+
}
|
|
206
|
+
const inspectExpression = String.raw`(async()=>{
|
|
207
|
+
await document.fonts.ready;
|
|
208
|
+
const root=document.getElementById('qb-rich-table-root');
|
|
209
|
+
if(!root||document.body.children.length!==1||document.body.firstElementChild!==root||[...document.body.childNodes].some(n=>n.nodeType===3&&n.textContent.trim()))throw new Error('static fragment escaped its capture container');
|
|
210
|
+
const container=root.firstElementChild;
|
|
211
|
+
if(root.children.length!==1||!container||container.tagName!=='DIV'||!/^[-a-zA-Z0-9_]+$/.test(container.id)||container.id==='qb-rich-table-root')throw new Error('one scoped gt container is required');
|
|
212
|
+
const styles=root.querySelectorAll('style');
|
|
213
|
+
if(styles.length!==1||styles[0].parentElement!==container)throw new Error('one original scoped gt stylesheet is required');
|
|
214
|
+
const allowed=new Set('DIV STYLE TABLE CAPTION COLGROUP COL THEAD TBODY TFOOT TR TH TD SPAN P STRONG EM B I SUP SUB BR HR ABBR'.split(' '));
|
|
215
|
+
const attributes=new Set('id class style colspan rowspan headers scope role aria-label aria-hidden title data-quarto-postprocess data-quarto-disable-processing data-quarto-bootstrap data-quarto-table-cell-role'.split(' '));
|
|
216
|
+
function fail(message){throw new Error(message)};
|
|
217
|
+
for(const e of root.querySelectorAll('*')){
|
|
218
|
+
if(!allowed.has(e.tagName))fail('unsupported static HTML tag '+e.tagName);
|
|
219
|
+
for(const a of e.attributes)if(!attributes.has(a.name))fail('unsupported static HTML attribute '+a.name);
|
|
220
|
+
const css=e.tagName==='STYLE'?e.textContent:e.getAttribute('style')||'';
|
|
221
|
+
if(/[\\@]|url\s*\(|image-set\s*\(/i.test(css))fail('external resources, font imports and escaped CSS need a faithful bundled adapter');
|
|
222
|
+
if(e.tagName==='STYLE'){
|
|
223
|
+
for(const rule of e.sheet.cssRules)if(rule.type!==CSSRule.STYLE_RULE||rule.selectorText.split(',').some(selector=>!selector.trim().startsWith('#'+container.id+' ')))fail('CSS must stay scoped to the original gt container');
|
|
224
|
+
}else{
|
|
225
|
+
const s=getComputedStyle(e);
|
|
226
|
+
if(s.animationName!=='none'||s.transitionDuration.split(',').some(v=>parseFloat(v)!==0)||['absolute','fixed','sticky'].includes(s.position)||s.transform!=='none'||s.scale!=='none'||s.rotate!=='none'||s.translate!=='none'||s.zoom!=='1'||s.filter!=='none'||s.clipPath!=='none'||s.maskImage!=='none'||s.boxShadow!=='none')fail('animated transformed or positioned HTML needs a separate layout adapter');
|
|
227
|
+
if(s.visibility!=='visible'||s.opacity!=='1')fail('hidden or transparent table content cannot be validated');
|
|
228
|
+
for(const pseudo of ['::before','::after'])if(!['none','normal'].includes(getComputedStyle(e,pseudo).content))fail('generated CSS content needs a separate content adapter');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const tables=root.querySelectorAll('table');if(tables.length!==1||!tables[0].classList.contains('gt_table'))fail('one gt table is required');
|
|
232
|
+
const box=root.getBoundingClientRect(),tableBox=tables[0].getBoundingClientRect();
|
|
233
|
+
if(tableBox.left<box.left-0.01||tableBox.right>box.right+0.01||tableBox.top<box.top-0.01||tableBox.bottom>box.bottom+0.01)fail('complete table box exceeds capture bounds');
|
|
234
|
+
let minFontSize=Infinity,minBodyFontSize=Infinity;
|
|
235
|
+
const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const ctx=canvas.getContext('2d');
|
|
236
|
+
function invisibleColor(color){ctx.clearRect(0,0,1,1);ctx.fillStyle=color;ctx.fillRect(0,0,1,1);return ctx.getImageData(0,0,1,1).data[3]===0;}
|
|
237
|
+
const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);
|
|
238
|
+
while(walker.nextNode()){
|
|
239
|
+
const n=walker.currentNode;if(!n.textContent.trim()||n.parentElement.tagName==='STYLE')continue;
|
|
240
|
+
const range=document.createRange();range.selectNodeContents(n);const rects=[...range.getClientRects()];
|
|
241
|
+
if(!rects.length)fail('text is hidden instead of captured');
|
|
242
|
+
const textStyle=getComputedStyle(n.parentElement);
|
|
243
|
+
if(invisibleColor(textStyle.color)||invisibleColor(textStyle.webkitTextFillColor))fail('transparent text cannot be validated');
|
|
244
|
+
const fontSize=parseFloat(textStyle.fontSize);
|
|
245
|
+
minFontSize=Math.min(minFontSize,fontSize);
|
|
246
|
+
const marker=n.parentElement.closest('.gt_footnote_marks');
|
|
247
|
+
if(!marker||marker.textContent.trim().length>6)minBodyFontSize=Math.min(minBodyFontSize,fontSize);
|
|
248
|
+
for(const r of rects){
|
|
249
|
+
if(!r.width||!r.height||r.left<box.left-0.01||r.right>box.right+0.01||r.top<box.top-0.01||r.bottom>box.bottom+0.01)fail('table text exceeds the complete capture bounds');
|
|
250
|
+
for(let e=n.parentElement;e&&root.contains(e);e=e.parentElement){
|
|
251
|
+
const s=getComputedStyle(e),b=e.getBoundingClientRect();
|
|
252
|
+
if(['hidden','clip','auto','scroll'].includes(s.overflowX)&&(r.left<b.left-0.01||r.right>b.right+0.01))fail('horizontal text clipping');
|
|
253
|
+
if(['hidden','clip','auto','scroll'].includes(s.overflowY)&&(r.top<b.top-0.01||r.bottom>b.bottom+0.01))fail('vertical text clipping');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if(minFontSize<8||minBodyFontSize<10||!Number.isFinite(minBodyFontSize))fail('table text is below the supported readable size');
|
|
258
|
+
const cells=[...tables[0].querySelectorAll('th,td')];
|
|
259
|
+
return {width:Math.ceil(box.width),height:Math.ceil(box.height),minFontSize,minBodyFontSize,rows:tables[0].rows.length,cells:cells.length,text:cells.map(e=>e.innerText).join('\n')};
|
|
260
|
+
})()`;
|
|
261
|
+
export async function snapshotRichTable(
|
|
262
|
+
candidate: RichTableCandidate,
|
|
263
|
+
options: { chromium?: string; timeoutMs?: number; maxHeight?: number } = {},
|
|
264
|
+
): Promise<RichTableSnapshot> {
|
|
265
|
+
const maxHeight = options.maxHeight ?? 534;
|
|
266
|
+
if (!Number.isSafeInteger(maxHeight) || maxHeight < 1 || maxHeight > 534) {
|
|
267
|
+
fail(
|
|
268
|
+
"remaining capture frame height must be an integer from 1 through 534",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const html = await richTableHtml(candidate);
|
|
272
|
+
if (html.length > 2_000_000) fail("HTML output exceeds 2 MB");
|
|
273
|
+
return await withHeadlessChromium(options, async (client) => {
|
|
274
|
+
await client.send("Page.enable");
|
|
275
|
+
await client.send("Runtime.enable");
|
|
276
|
+
await client.send("Network.enable");
|
|
277
|
+
await client.send("Network.setBlockedURLs", { urls: ["*"] });
|
|
278
|
+
await client.send("Emulation.setScriptExecutionDisabled", { value: true });
|
|
279
|
+
await client.send("Emulation.setDeviceMetricsOverride", {
|
|
280
|
+
width: 1120,
|
|
281
|
+
height: 720,
|
|
282
|
+
deviceScaleFactor: 1,
|
|
283
|
+
mobile: false,
|
|
284
|
+
});
|
|
285
|
+
const tree = await client.send<{ frameTree: { frame: { id: string } } }>(
|
|
286
|
+
"Page.getFrameTree",
|
|
287
|
+
);
|
|
288
|
+
await client.send("Page.setDocumentContent", {
|
|
289
|
+
frameId: tree.frameTree.frame.id,
|
|
290
|
+
html:
|
|
291
|
+
`<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';"><style>html,body{margin:0;padding:0;background:white}#qb-rich-table-root{display:flow-root;width:1120px}</style><div id="qb-rich-table-root">${html}</div>`,
|
|
292
|
+
});
|
|
293
|
+
const evaluated = await client.send<
|
|
294
|
+
{
|
|
295
|
+
result: {
|
|
296
|
+
value: {
|
|
297
|
+
width: number;
|
|
298
|
+
height: number;
|
|
299
|
+
rows: number;
|
|
300
|
+
cells: number;
|
|
301
|
+
minFontSize: number;
|
|
302
|
+
minBodyFontSize: number;
|
|
303
|
+
text: string;
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
exceptionDetails?: unknown;
|
|
307
|
+
}
|
|
308
|
+
>("Runtime.evaluate", {
|
|
309
|
+
expression: inspectExpression,
|
|
310
|
+
returnByValue: true,
|
|
311
|
+
awaitPromise: true,
|
|
312
|
+
});
|
|
313
|
+
if (evaluated.exceptionDetails) {
|
|
314
|
+
fail(
|
|
315
|
+
`static capture validation failed: ${
|
|
316
|
+
canonical(evaluated.exceptionDetails)
|
|
317
|
+
}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const measured = evaluated.result.value;
|
|
321
|
+
if (
|
|
322
|
+
measured.width !== 1120 || measured.height < 1 ||
|
|
323
|
+
measured.height > maxHeight
|
|
324
|
+
) {
|
|
325
|
+
fail(
|
|
326
|
+
"complete table exceeds the remaining slide frame; split it in the source, do not clip or shrink it",
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (client.events.some((e) => e.method === "Network.requestWillBeSent")) {
|
|
330
|
+
fail(
|
|
331
|
+
"static table attempted to request a resource; offline dependency bundling is unsupported",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const capture = await client.send<{ data: string }>(
|
|
335
|
+
"Page.captureScreenshot",
|
|
336
|
+
{
|
|
337
|
+
format: "png",
|
|
338
|
+
captureBeyondViewport: true,
|
|
339
|
+
clip: { x: 0, y: 0, width: 1120, height: measured.height, scale: 2 },
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
const png = Uint8Array.from(atob(capture.data), (c) => c.charCodeAt(0));
|
|
343
|
+
const digest = await crypto.subtle.digest("SHA-256", png);
|
|
344
|
+
const pngHash = Array.from(
|
|
345
|
+
new Uint8Array(digest),
|
|
346
|
+
(b) => b.toString(16).padStart(2, "0"),
|
|
347
|
+
).join("");
|
|
348
|
+
return {
|
|
349
|
+
candidate,
|
|
350
|
+
png,
|
|
351
|
+
diagnostic: {
|
|
352
|
+
code: "STATIC_TABLE_PNG_FALLBACK",
|
|
353
|
+
message:
|
|
354
|
+
"This computed gt table is preserved as a static 2x PNG, including its executed formatting. It is raster, not a native editable table or vector image; change its data and formatting in the source producer.",
|
|
355
|
+
},
|
|
356
|
+
receipt: {
|
|
357
|
+
version: 1,
|
|
358
|
+
kind: "gt-static-png",
|
|
359
|
+
source: structuredClone(candidate.source),
|
|
360
|
+
htmlHash: hashSource(html),
|
|
361
|
+
pngHash,
|
|
362
|
+
textHash: hashSource(measured.text),
|
|
363
|
+
browser: client.version,
|
|
364
|
+
width: 1120,
|
|
365
|
+
height: measured.height,
|
|
366
|
+
pixelRatio: 2,
|
|
367
|
+
rows: measured.rows,
|
|
368
|
+
cells: measured.cells,
|
|
369
|
+
minFontSize: measured.minFontSize,
|
|
370
|
+
minBodyFontSize: measured.minBodyFontSize,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
});
|
|
374
|
+
}
|