@brianmsm/quarto-bento 0.0.0-stage → 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 +21 -0
- package/README.md +86 -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 +219 -0
- package/distribution/runtime/src/core/agent.ts +565 -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 +759 -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 +2423 -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 +790 -0
- package/distribution/runtime/src/studio/components.js +188 -0
- package/distribution/runtime/src/studio/index.html +278 -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 +433 -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,367 @@
|
|
|
1
|
+
import { canonical } from "../state/json.ts";
|
|
2
|
+
import type { Element, Origin } from "../render/types.ts";
|
|
3
|
+
import { scanSource, type SourceDocument } from "../source/mod.ts";
|
|
4
|
+
import { hashSource } from "../source/mod.ts";
|
|
5
|
+
import { fail, parsePlotlyPayload, type PlotlyCandidate } from "./plotly.ts";
|
|
6
|
+
|
|
7
|
+
/** Exact local runtime proved by this adapter, never fetched from a CDN. */
|
|
8
|
+
export const PLOTLY_RUNTIME = {
|
|
9
|
+
version: "2.25.2",
|
|
10
|
+
sha256: "419deebe105f4c993feb7e6d1fe94fb1c2f7f98716db5d9b8fe3e37773838898",
|
|
11
|
+
licenseSha256:
|
|
12
|
+
"67a26cf80f03ff388f26945dfc1f6caed1a0746ff43871190339b3aee49b94bb",
|
|
13
|
+
} as const;
|
|
14
|
+
export interface PlotlyRuntime {
|
|
15
|
+
javascript: string;
|
|
16
|
+
license: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PlotlyBundle {
|
|
19
|
+
version: 1;
|
|
20
|
+
candidate: PlotlyCandidate;
|
|
21
|
+
runtime: typeof PLOTLY_RUNTIME;
|
|
22
|
+
fallback: { mime: "image/svg+xml"; svg: string; sha256: string };
|
|
23
|
+
}
|
|
24
|
+
/** Store separately in the trusted generation receipt, never load from the bundle. */
|
|
25
|
+
export interface PlotlyReceipt {
|
|
26
|
+
version: 1;
|
|
27
|
+
bundleHash: string;
|
|
28
|
+
source: PlotlyCandidate["source"];
|
|
29
|
+
specHash: string;
|
|
30
|
+
runtimeSha256: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PlotlyContext {
|
|
33
|
+
source: SourceDocument;
|
|
34
|
+
expectedReceipt: PlotlyReceipt;
|
|
35
|
+
}
|
|
36
|
+
const encode = (value: string) => {
|
|
37
|
+
let binary = "";
|
|
38
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
39
|
+
binary += String.fromCharCode(byte);
|
|
40
|
+
}
|
|
41
|
+
return btoa(binary);
|
|
42
|
+
};
|
|
43
|
+
const html = (value: string) =>
|
|
44
|
+
value.replaceAll("&", "&").replaceAll('"', """).replaceAll(
|
|
45
|
+
"<",
|
|
46
|
+
"<",
|
|
47
|
+
).replaceAll(">", ">");
|
|
48
|
+
const json = (value: unknown) =>
|
|
49
|
+
JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
50
|
+
async function sha256(value: string): Promise<string> {
|
|
51
|
+
return [
|
|
52
|
+
...new Uint8Array(
|
|
53
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)),
|
|
54
|
+
),
|
|
55
|
+
].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
56
|
+
}
|
|
57
|
+
export async function verifyPlotlyRuntime(
|
|
58
|
+
runtime: PlotlyRuntime,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
if (
|
|
61
|
+
await sha256(runtime.javascript) !== PLOTLY_RUNTIME.sha256 ||
|
|
62
|
+
await sha256(runtime.license) !== PLOTLY_RUNTIME.licenseSha256
|
|
63
|
+
) fail("Plotly runtime or license differs from the verified local pin");
|
|
64
|
+
}
|
|
65
|
+
/** Deliberately rejects active elements, hyperlinks, images and nonlocal CSS refs. */
|
|
66
|
+
export function verifyStaticSvg(svg: string): void {
|
|
67
|
+
if (
|
|
68
|
+
svg.length > 5_000_000 || !/^<svg\s/.test(svg) || !/<\/svg>\s*$/.test(svg)
|
|
69
|
+
) fail("invalid or oversized SVG snapshot");
|
|
70
|
+
if (
|
|
71
|
+
/<!|<\?|\bon[a-z]+\s*=|(?:href|src|xml:base)\s*=|@import|\\|&(?!(?:amp|lt|gt|quot|apos);)/i
|
|
72
|
+
.test(svg)
|
|
73
|
+
) fail("active or external SVG content");
|
|
74
|
+
const tags = new Set(
|
|
75
|
+
"svg g defs clipPath rect path line polyline polygon circle ellipse text tspan title desc"
|
|
76
|
+
.split(" "),
|
|
77
|
+
);
|
|
78
|
+
for (const matched of svg.matchAll(/<\/?([A-Za-z][\w:-]*)\b/g)) {
|
|
79
|
+
if (!tags.has(matched[1])) {
|
|
80
|
+
fail(`SVG element ${matched[1]} is not supported`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const attributes = new Set(
|
|
84
|
+
"xmlns xmlns:xlink class id d x y x1 y1 x2 y2 cx cy r rx ry dx dy height width viewBox transform style clip-path text-anchor dominant-baseline fill fill-opacity stroke stroke-width stroke-opacity stroke-linecap stroke-linejoin stroke-miterlimit stroke-dasharray stroke-dashoffset opacity font-family font-size font-weight font-style vector-effect points"
|
|
85
|
+
.split(" "),
|
|
86
|
+
);
|
|
87
|
+
const properties = new Set(
|
|
88
|
+
"font-family font-size font-weight font-style fill fill-opacity stroke stroke-width stroke-opacity stroke-linecap stroke-linejoin stroke-miterlimit stroke-dasharray stroke-dashoffset opacity white-space vector-effect display visibility paint-order"
|
|
89
|
+
.split(" "),
|
|
90
|
+
);
|
|
91
|
+
for (const tag of svg.matchAll(/<([A-Za-z][\w:-]*)([^>]*)>/g)) {
|
|
92
|
+
let rest = tag[2].replace(/\/\s*$/, "");
|
|
93
|
+
while (rest.trim()) {
|
|
94
|
+
const attribute = rest.match(/^\s+([\w:-]+)\s*=\s*("[^"]*"|'[^']*')/);
|
|
95
|
+
if (!attribute || !attributes.has(attribute[1])) {
|
|
96
|
+
fail("unsupported SVG attribute syntax");
|
|
97
|
+
}
|
|
98
|
+
const name = attribute[1], value = attribute[2].slice(1, -1);
|
|
99
|
+
if (name === "style") {
|
|
100
|
+
for (
|
|
101
|
+
const declaration of value.split(";").map((v) => v.trim()).filter(
|
|
102
|
+
Boolean,
|
|
103
|
+
)
|
|
104
|
+
) {
|
|
105
|
+
const colon = declaration.indexOf(":");
|
|
106
|
+
if (
|
|
107
|
+
colon < 0 || !properties.has(declaration.slice(0, colon).trim())
|
|
108
|
+
) fail("unsupported SVG style property");
|
|
109
|
+
const css = declaration.slice(colon + 1);
|
|
110
|
+
if (
|
|
111
|
+
/[{}@&]/.test(css) ||
|
|
112
|
+
[...css.matchAll(/([\w-]+)\s*\(/g)].some((m) =>
|
|
113
|
+
!["rgb", "rgba", "url"].includes(m[1])
|
|
114
|
+
)
|
|
115
|
+
) fail("unsupported SVG style function");
|
|
116
|
+
}
|
|
117
|
+
} else if (["fill", "stroke", "clip-path"].includes(name)) {
|
|
118
|
+
if (
|
|
119
|
+
/[{}@&]/.test(value) ||
|
|
120
|
+
[...value.matchAll(/([\w-]+)\s*\(/g)].some((m) =>
|
|
121
|
+
!["rgb", "rgba", "url"].includes(m[1])
|
|
122
|
+
)
|
|
123
|
+
) fail("unsupported SVG presentation function");
|
|
124
|
+
}
|
|
125
|
+
rest = rest.slice(attribute[0].length);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const matched of svg.matchAll(/url\s*\(([^)]*)\)/gi)) {
|
|
129
|
+
if (!/^#[A-Za-z0-9_.:-]+$/.test(matched[1])) {
|
|
130
|
+
fail("SVG CSS reference is not an internal fragment");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function verifyCandidate(candidate: PlotlyCandidate) {
|
|
135
|
+
if (
|
|
136
|
+
candidate.version !== 1 || candidate.kind !== "plotly" ||
|
|
137
|
+
candidate.specHash !== hashSource(canonical(candidate.spec))
|
|
138
|
+
) fail("widget candidate changed after projection");
|
|
139
|
+
const config = structuredClone(candidate.spec.config);
|
|
140
|
+
if (
|
|
141
|
+
config.responsive !== false || config.displaylogo !== false ||
|
|
142
|
+
config.plotGlPixelRatio !== 1 ||
|
|
143
|
+
JSON.stringify(config.modeBarButtonsToRemove) !==
|
|
144
|
+
JSON.stringify(["toImage", "sendDataToCloud"])
|
|
145
|
+
) fail("widget config changed after projection");
|
|
146
|
+
delete config.responsive;
|
|
147
|
+
delete config.plotGlPixelRatio;
|
|
148
|
+
delete config.modeBarButtonsToRemove;
|
|
149
|
+
const checked = parsePlotlyPayload(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
x: { data: candidate.spec.data, layout: candidate.spec.layout, config },
|
|
152
|
+
evals: [],
|
|
153
|
+
jsHooks: [],
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
if (canonical(checked) !== canonical(candidate.spec)) {
|
|
157
|
+
fail("widget spec changed after projection");
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
!Number.isInteger(candidate.width) || !Number.isInteger(candidate.height) ||
|
|
161
|
+
candidate.width < 100 || candidate.width > 1600 || candidate.height < 100 ||
|
|
162
|
+
candidate.height > 1600
|
|
163
|
+
) fail("invalid widget dimensions");
|
|
164
|
+
}
|
|
165
|
+
export function createPlotlyBundle(
|
|
166
|
+
candidate: PlotlyCandidate,
|
|
167
|
+
svg: string,
|
|
168
|
+
): PlotlyBundle {
|
|
169
|
+
verifyCandidate(candidate);
|
|
170
|
+
verifyStaticSvg(svg);
|
|
171
|
+
return {
|
|
172
|
+
version: 1,
|
|
173
|
+
candidate: structuredClone(candidate),
|
|
174
|
+
runtime: PLOTLY_RUNTIME,
|
|
175
|
+
fallback: { mime: "image/svg+xml", svg, sha256: hashSource(svg) },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function innerPage(
|
|
179
|
+
candidate: PlotlyCandidate,
|
|
180
|
+
runtime: PlotlyRuntime,
|
|
181
|
+
nonce: string,
|
|
182
|
+
exportSvg: boolean,
|
|
183
|
+
) {
|
|
184
|
+
const csp =
|
|
185
|
+
`default-src 'none'; script-src 'nonce-${nonce}' data:; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'; worker-src 'none'`;
|
|
186
|
+
const script = `"use strict";
|
|
187
|
+
const spec=${
|
|
188
|
+
json(candidate.spec)
|
|
189
|
+
}, width=${candidate.width}, height=${candidate.height};
|
|
190
|
+
window.PlotlyConfig={MathJaxConfig:'local'};
|
|
191
|
+
const chart=document.getElementById('chart');
|
|
192
|
+
const report=value=>parent.postMessage({qbentoWidget:${
|
|
193
|
+
json(nonce)
|
|
194
|
+
},...value},'*');
|
|
195
|
+
const layout={...spec.layout,width,height};
|
|
196
|
+
Plotly.newPlot(chart,spec.data,layout,spec.config).then(async()=>{
|
|
197
|
+
if(${exportSvg}){
|
|
198
|
+
const uri=await Plotly.toImage(chart,{format:'svg',width,height});
|
|
199
|
+
if(!uri.startsWith('data:image/svg+xml,'))throw new Error('Expected an SVG data URI');
|
|
200
|
+
report({svg:decodeURIComponent(uri.slice(uri.indexOf(',')+1)),ready:true});
|
|
201
|
+
}else report({ready:true});
|
|
202
|
+
}).catch(error=>report({error:String(error)}));`;
|
|
203
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${
|
|
204
|
+
html(csp)
|
|
205
|
+
}"><style>html,body{margin:0;width:100%;height:100%;overflow:hidden}#chart{width:${candidate.width}px;height:${candidate.height}px}</style></head><body><div id="chart"></div><script nonce="${nonce}" src="data:text/javascript;base64,${
|
|
206
|
+
encode(runtime.javascript)
|
|
207
|
+
}"></script><script nonce="${nonce}">${script}</script></body></html>`;
|
|
208
|
+
}
|
|
209
|
+
/** Isolated opaque-origin child; no same-origin, forms, downloads, popups or navigation grants. */
|
|
210
|
+
async function page(
|
|
211
|
+
candidate: PlotlyCandidate,
|
|
212
|
+
runtime: PlotlyRuntime,
|
|
213
|
+
bundle?: PlotlyBundle,
|
|
214
|
+
): Promise<string> {
|
|
215
|
+
verifyCandidate(candidate);
|
|
216
|
+
await verifyPlotlyRuntime(runtime);
|
|
217
|
+
const nonce = crypto.randomUUID().replaceAll("-", "");
|
|
218
|
+
const csp =
|
|
219
|
+
`default-src 'none'; script-src 'nonce-${nonce}' data:; style-src 'unsafe-inline'; img-src data:; frame-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; worker-src 'none'`;
|
|
220
|
+
const child = innerPage(candidate, runtime, nonce, !bundle);
|
|
221
|
+
const fallback = bundle
|
|
222
|
+
? `<img id="fallback" alt="Gráfico calculado; versión estática" width="${candidate.width}" height="${candidate.height}" src="data:image/svg+xml;base64,${
|
|
223
|
+
encode(bundle.fallback.svg)
|
|
224
|
+
}">`
|
|
225
|
+
: "";
|
|
226
|
+
const controls = bundle
|
|
227
|
+
? '<button id="activate" disabled>Activar gráfico interactivo</button><p id="status" role="status">Cargando recursos locales…</p>'
|
|
228
|
+
: '<p id="status">Preparando SVG…</p>';
|
|
229
|
+
const script = `"use strict";const iframe=document.getElementById('widget');
|
|
230
|
+
const result=document.getElementById('qb-widget-result'),status=document.getElementById('status');
|
|
231
|
+
window.addEventListener('message',event=>{
|
|
232
|
+
if(event.source!==iframe.contentWindow||event.origin!=='null'||event.data?.qbentoWidget!==${
|
|
233
|
+
json(nonce)
|
|
234
|
+
})return;
|
|
235
|
+
if(event.data.error){status.textContent='No se pudo representar el widget: '+event.data.error;result.textContent=JSON.stringify({error:event.data.error});return;}
|
|
236
|
+
if(event.data.ready){status.textContent='Listo · recursos incluidos · sin conexión';result.textContent=JSON.stringify({ready:true,svg:event.data.svg});const button=document.getElementById('activate');if(button)button.disabled=false;}
|
|
237
|
+
});
|
|
238
|
+
const button=document.getElementById('activate');if(button)button.addEventListener('click',()=>{const active=iframe.hidden;iframe.hidden=!active;document.getElementById('fallback').hidden=active;button.textContent=active?'Mostrar versión estática':'Activar gráfico interactivo';});
|
|
239
|
+
iframe.srcdoc=${json(child)};`;
|
|
240
|
+
return `<!doctype html><html lang="es"><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta http-equiv="Content-Security-Policy" content="${
|
|
241
|
+
html(csp)
|
|
242
|
+
}"><title>Plotly · Presenter aislado</title><style>body{margin:24px;font:16px system-ui;background:#f4f6f7;color:#172a35}main{max-width:${candidate.width}px;margin:auto}iframe{border:0;background:white}button{padding:10px 16px}img{display:block}img[hidden],iframe[hidden]{display:none}@media print{body{margin:0}button,#status,details,iframe{display:none!important}#fallback{display:block!important}}details{margin-top:20px}pre{white-space:pre-wrap;font-size:12px}</style></head><body><main>${controls}${fallback}<iframe id="widget" title="Gráfico Plotly aislado" sandbox="allow-scripts" referrerpolicy="no-referrer" width="${candidate.width}" height="${candidate.height}" ${
|
|
243
|
+
bundle ? "hidden" : ""
|
|
244
|
+
}></iframe><script id="qb-widget-result" type="application/json">{}</script><details><summary>Plotly ${PLOTLY_RUNTIME.version} · licencia</summary><pre>${
|
|
245
|
+
html(runtime.license)
|
|
246
|
+
}</pre></details></main><script nonce="${nonce}">${script}</script></body></html>`;
|
|
247
|
+
}
|
|
248
|
+
export const createPlotlySnapshotPage = (
|
|
249
|
+
candidate: PlotlyCandidate,
|
|
250
|
+
runtime: PlotlyRuntime,
|
|
251
|
+
): Promise<string> => page(candidate, runtime);
|
|
252
|
+
/** Call only for a newly captured bundle, before its generation is published. */
|
|
253
|
+
export function createPlotlyReceipt(bundle: PlotlyBundle): PlotlyReceipt {
|
|
254
|
+
verifyCandidate(bundle.candidate);
|
|
255
|
+
verifyStaticSvg(bundle.fallback.svg);
|
|
256
|
+
if (
|
|
257
|
+
bundle.version !== 1 ||
|
|
258
|
+
bundle.fallback.sha256 !== hashSource(bundle.fallback.svg) ||
|
|
259
|
+
canonical(bundle.runtime) !== canonical(PLOTLY_RUNTIME)
|
|
260
|
+
) fail("invalid freshly captured widget bundle");
|
|
261
|
+
return {
|
|
262
|
+
version: 1,
|
|
263
|
+
bundleHash: hashSource(canonical(bundle)),
|
|
264
|
+
source: structuredClone(bundle.candidate.source),
|
|
265
|
+
specHash: bundle.candidate.specHash,
|
|
266
|
+
runtimeSha256: PLOTLY_RUNTIME.sha256,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** Validate against current QMD and a receipt obtained from the trusted core generation. */
|
|
270
|
+
function verifyBundleContext(
|
|
271
|
+
bundle: PlotlyBundle,
|
|
272
|
+
context: PlotlyContext,
|
|
273
|
+
): void {
|
|
274
|
+
const receipt = createPlotlyReceipt(bundle);
|
|
275
|
+
if (canonical(receipt) !== canonical(context.expectedReceipt)) {
|
|
276
|
+
fail("bundle does not match the expected generation receipt");
|
|
277
|
+
}
|
|
278
|
+
const original = context.source;
|
|
279
|
+
if (
|
|
280
|
+
hashSource(original.text) !== original.hash ||
|
|
281
|
+
original.hash !== receipt.source.sourceHash
|
|
282
|
+
) fail("widget source snapshot is stale");
|
|
283
|
+
const source = scanSource(original.text);
|
|
284
|
+
if (source.diagnostics.length) {
|
|
285
|
+
fail("source mapping diagnostics must be resolved first");
|
|
286
|
+
}
|
|
287
|
+
const wrapper = source.blocks.find((b) =>
|
|
288
|
+
b.id === receipt.source.sourceId && b.kind === "div" && b.stable
|
|
289
|
+
);
|
|
290
|
+
const children = wrapper
|
|
291
|
+
? source.blocks.filter((b) => b.parentId === wrapper.id)
|
|
292
|
+
: [];
|
|
293
|
+
if (
|
|
294
|
+
children.length !== 1 || children[0].kind !== "chunk" ||
|
|
295
|
+
!children[0].stable || children[0].id !== receipt.source.producerId ||
|
|
296
|
+
children[0].slideId !== receipt.source.slideId ||
|
|
297
|
+
hashSource(children[0].raw) !== receipt.source.chunkHash
|
|
298
|
+
) fail("widget producer no longer matches original source");
|
|
299
|
+
if (/results\s*[:=]\s*["']?asis|asis_output\s*\(/i.test(children[0].raw)) {
|
|
300
|
+
fail("unsupported asis producer");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** Loading untrusted persisted JSON always requires both fresh QMD and an external receipt. */
|
|
304
|
+
export async function loadPlotlyBundle(
|
|
305
|
+
serialized: string,
|
|
306
|
+
options: PlotlyContext & { runtime: PlotlyRuntime },
|
|
307
|
+
): Promise<PlotlyBundle> {
|
|
308
|
+
if (serialized.length > 8_000_000) fail("widget bundle exceeds 8 MB");
|
|
309
|
+
let bundle: PlotlyBundle;
|
|
310
|
+
try {
|
|
311
|
+
bundle = JSON.parse(serialized);
|
|
312
|
+
verifyBundleContext(bundle, options);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (
|
|
315
|
+
error instanceof Error && error.message.startsWith("UNSUPPORTED_WIDGET:")
|
|
316
|
+
) throw error;
|
|
317
|
+
fail("malformed widget bundle or context");
|
|
318
|
+
}
|
|
319
|
+
await verifyPlotlyRuntime(options.runtime);
|
|
320
|
+
return bundle!;
|
|
321
|
+
}
|
|
322
|
+
export function createPlotlyPresenter(
|
|
323
|
+
bundle: PlotlyBundle,
|
|
324
|
+
runtime: PlotlyRuntime,
|
|
325
|
+
context: PlotlyContext,
|
|
326
|
+
): Promise<string> {
|
|
327
|
+
verifyBundleContext(bundle, context);
|
|
328
|
+
if (
|
|
329
|
+
bundle.version !== 1 || bundle.runtime.sha256 !== PLOTLY_RUNTIME.sha256 ||
|
|
330
|
+
bundle.fallback.sha256 !== hashSource(bundle.fallback.svg)
|
|
331
|
+
) fail("widget bundle integrity mismatch");
|
|
332
|
+
verifyStaticSvg(bundle.fallback.svg);
|
|
333
|
+
return page(bundle.candidate, runtime, bundle);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Integration boundary: only the checked static image enters pure-data BentoDoc. */
|
|
337
|
+
export function plotlyStaticElement(
|
|
338
|
+
bundle: PlotlyBundle,
|
|
339
|
+
context: PlotlyContext,
|
|
340
|
+
): { element: Element; origin: Origin } {
|
|
341
|
+
verifyBundleContext(bundle, context);
|
|
342
|
+
if (bundle.fallback.sha256 !== hashSource(bundle.fallback.svg)) {
|
|
343
|
+
fail("widget fallback changed");
|
|
344
|
+
}
|
|
345
|
+
verifyCandidate(bundle.candidate);
|
|
346
|
+
verifyStaticSvg(bundle.fallback.svg);
|
|
347
|
+
const c = bundle.candidate;
|
|
348
|
+
return {
|
|
349
|
+
element: {
|
|
350
|
+
id: c.source.sourceId,
|
|
351
|
+
type: "image",
|
|
352
|
+
x: 160,
|
|
353
|
+
y: 130,
|
|
354
|
+
w: 880,
|
|
355
|
+
h: Math.min(480, c.height),
|
|
356
|
+
rotation: 0,
|
|
357
|
+
opacity: 1,
|
|
358
|
+
src: `data:image/svg+xml;base64,${encode(bundle.fallback.svg)}`,
|
|
359
|
+
fit: "contain",
|
|
360
|
+
},
|
|
361
|
+
origin: {
|
|
362
|
+
origin: "computed",
|
|
363
|
+
sourceId: c.source.sourceId,
|
|
364
|
+
producer: c.source.producerId,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Optional local Chromium exporter; requires a compatible browser already installed. */
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import {
|
|
4
|
+
createPlotlyBundle,
|
|
5
|
+
createPlotlySnapshotPage,
|
|
6
|
+
type PlotlyBundle,
|
|
7
|
+
type PlotlyRuntime,
|
|
8
|
+
verifyStaticSvg,
|
|
9
|
+
} from "./presenter.ts";
|
|
10
|
+
import { fail, type PlotlyCandidate } from "./plotly.ts";
|
|
11
|
+
export async function snapshotPlotly(
|
|
12
|
+
candidate: PlotlyCandidate,
|
|
13
|
+
runtime: PlotlyRuntime,
|
|
14
|
+
options: { chromium?: string; timeoutMs?: number } = {},
|
|
15
|
+
): Promise<PlotlyBundle> {
|
|
16
|
+
const page = await createPlotlySnapshotPage(candidate, runtime);
|
|
17
|
+
const dir = await Deno.makeTempDir({ prefix: "qbento-plotly-snapshot-" });
|
|
18
|
+
let child: Deno.ChildProcess | undefined;
|
|
19
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
20
|
+
try {
|
|
21
|
+
await Deno.writeTextFile(`${dir}/snapshot.html`, page);
|
|
22
|
+
child = new Deno.Command(options.chromium ?? "chromium", {
|
|
23
|
+
args: [
|
|
24
|
+
"--headless",
|
|
25
|
+
"--no-first-run",
|
|
26
|
+
"--no-default-browser-check",
|
|
27
|
+
"--disable-background-networking",
|
|
28
|
+
"--disable-extensions",
|
|
29
|
+
"--disable-sync",
|
|
30
|
+
"--no-pings",
|
|
31
|
+
`--user-data-dir=${dir}/profile`,
|
|
32
|
+
"--dump-dom",
|
|
33
|
+
"--virtual-time-budget=5000",
|
|
34
|
+
pathToFileURL(`${dir}/snapshot.html`).href,
|
|
35
|
+
],
|
|
36
|
+
stdout: "piped",
|
|
37
|
+
stderr: "piped",
|
|
38
|
+
}).spawn();
|
|
39
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
try {
|
|
42
|
+
child?.kill("SIGKILL");
|
|
43
|
+
} catch { /* Already exited. */ }
|
|
44
|
+
reject(
|
|
45
|
+
new Error(
|
|
46
|
+
"WIDGET_SNAPSHOT_TIMEOUT: local Chromium did not produce a snapshot",
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}, options.timeoutMs ?? 30_000);
|
|
50
|
+
});
|
|
51
|
+
const output = await Promise.race([child.output(), timeout]);
|
|
52
|
+
if (!output.success) {
|
|
53
|
+
fail(
|
|
54
|
+
`local Chromium export failed: ${
|
|
55
|
+
new TextDecoder().decode(output.stderr).slice(-2000)
|
|
56
|
+
}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const dom = new TextDecoder().decode(output.stdout);
|
|
60
|
+
const matched = dom.match(
|
|
61
|
+
/<script id="qb-widget-result" type="application\/json">([\s\S]*?)<\/script>/,
|
|
62
|
+
);
|
|
63
|
+
if (!matched) fail("Chromium did not emit an export result");
|
|
64
|
+
const result = JSON.parse(matched![1]);
|
|
65
|
+
if (result.ready !== true || typeof result.svg !== "string") {
|
|
66
|
+
fail(
|
|
67
|
+
`Chromium did not finish SVG export${
|
|
68
|
+
result.error ? `: ${result.error}` : ""
|
|
69
|
+
}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return createPlotlyBundle(
|
|
73
|
+
candidate,
|
|
74
|
+
canonicalPlotlySvg(result.svg, candidate.specHash),
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
78
|
+
await Deno.remove(dir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Plotly assigns random SVG IDs/trace class tokens; never rewrite user label text. */
|
|
83
|
+
export function canonicalPlotlySvg(svg: string, specHash: string): string {
|
|
84
|
+
verifyStaticSvg(svg);
|
|
85
|
+
const ids = new Map<string, string>();
|
|
86
|
+
const tags = (
|
|
87
|
+
input: string,
|
|
88
|
+
update: (name: string, value: string) => string,
|
|
89
|
+
) =>
|
|
90
|
+
input.replace(
|
|
91
|
+
/<[A-Za-z][^<>]*>/g,
|
|
92
|
+
(tag) =>
|
|
93
|
+
tag.replace(/\s([\w:-]+)=(["'])(.*?)\2/g, (_, name, quote, value) =>
|
|
94
|
+
` ${name}=${quote}${update(name, value)}${quote}`),
|
|
95
|
+
);
|
|
96
|
+
tags(svg, (name, value) => {
|
|
97
|
+
if (name === "id") {
|
|
98
|
+
if (ids.has(value)) fail("duplicate SVG identity");
|
|
99
|
+
ids.set(value, `qb-plotly-${specHash.slice(0, 16)}-${ids.size}`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
});
|
|
103
|
+
svg = tags(svg, (name, value) => {
|
|
104
|
+
if (name === "id") return ids.get(value)!;
|
|
105
|
+
if (name === "class") {
|
|
106
|
+
return value.split(/\s+/).filter((c) => !/^trace[0-9a-f]{6}$/.test(c))
|
|
107
|
+
.join(" ");
|
|
108
|
+
}
|
|
109
|
+
return value.replace(/url\(#([^)]+)\)/g, (_, id) => {
|
|
110
|
+
if (!ids.has(id)) fail("unbound SVG fragment");
|
|
111
|
+
return `url(#${ids.get(id)})`;
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
verifyStaticSvg(svg);
|
|
115
|
+
return svg;
|
|
116
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Embedded font provenance and notices
|
|
2
|
+
|
|
3
|
+
This note supplements the pinned Bento files. It records the embedded font
|
|
4
|
+
assets and the exact official OFL text sources used by this package. The
|
|
5
|
+
upstream `THIRD_PARTY_NOTICES.md` and `shell.html` remain byte-for-byte
|
|
6
|
+
unchanged.
|
|
7
|
+
|
|
8
|
+
## Embedded assets
|
|
9
|
+
|
|
10
|
+
| Font asset in Bento | Subset and size | Asset SHA-256 | Release version |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| Fraunces `FRAUNCES_900` | Latin, 34,632-byte WOFF2 | `628051c1bd196ee82e88cb02015e46b8fc30f505215b424d29627ec613395a0d` | Unknown |
|
|
13
|
+
| Instrument Sans `INSTRUMENT_VAR` | Latin, 29,904-byte WOFF2 | `6219bc4bfdfc5d9b2201dcdf046218b122a758f932e25ed5f168f929b7ca2311` | Unknown |
|
|
14
|
+
|
|
15
|
+
The byte counts and asset hashes come from the base64 WOFF2 data in Bento's
|
|
16
|
+
pinned `slides/src/fontdata.ts` at
|
|
17
|
+
`3d6f7f1fcdf2308e875345ce4f1e0ec0820c914b`. Its source comments identify the
|
|
18
|
+
families and Latin subsets. No upstream font release identifier is present in
|
|
19
|
+
that pinned source; the package records the release versions as unknown and
|
|
20
|
+
does not infer them from the font binary metadata.
|
|
21
|
+
|
|
22
|
+
## License text sources
|
|
23
|
+
|
|
24
|
+
The complete upstream license texts are included at the paths below and
|
|
25
|
+
integrity-pinned in `pin.json`. Each source URL includes an immutable upstream
|
|
26
|
+
Git commit, and the SHA-256 covers the exact vendored file bytes.
|
|
27
|
+
|
|
28
|
+
| Font | License file | Official source at immutable commit | SHA-256 |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| Fraunces | `font-licenses/fraunces-OFL.txt` | https://raw.githubusercontent.com/undercasetype/Fraunces/284bc5ea73b1233a05d6ec644e641b9404cba8a0/OFL.txt | `bdf4c22802eaf804f998195871c6b8938aac2ac14b2d78a8bd66a6f1eced833b` |
|
|
31
|
+
| Instrument Sans | `font-licenses/instrument-sans-OFL.txt` | https://raw.githubusercontent.com/Instrument/instrument-sans/24690a4ab2f92d45aa23246abeba515712145d75/OFL.txt | `9e27a72ed30eb49a08678f6a5d6ed98ec7ba5368f541637ee0683ec9134ef966` |
|
|
32
|
+
|
|
33
|
+
## Attribution evidence
|
|
34
|
+
|
|
35
|
+
Bento's pinned `THIRD_PARTY_NOTICES.md` attributes Fraunces to
|
|
36
|
+
`Copyright 2020 The Fraunces Project Authors`. The official Fraunces OFL text
|
|
37
|
+
included here begins `Copyright 2018 The Fraunces Project Authors`. The
|
|
38
|
+
upstream Fraunces commit at
|
|
39
|
+
https://github.com/undercasetype/Fraunces/commit/284bc5ea73b1233a05d6ec644e641b9404cba8a0
|
|
40
|
+
records the project-author attribution change; the license header still says
|
|
41
|
+
2018. This package records both observations without editing or replacing
|
|
42
|
+
Bento's supplied attribution.
|
|
43
|
+
|
|
44
|
+
For Instrument Sans, Bento's notice and the included official OFL both state
|
|
45
|
+
`Copyright 2022 The Instrument Sans Project Authors`.
|
|
46
|
+
|
|
47
|
+
This is provenance and notice coverage for these two embedded fonts only. It
|
|
48
|
+
does not constitute a complete legal review of Bento, its runtime components,
|
|
49
|
+
the font binaries' exact upstream releases, or Quarto-Bento's own licensing.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Bento authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
bento/slides is MIT-licensed (© 2026 The Bento authors; see `LICENSE`).
|
|
4
|
+
The shippable single-file shell (`Bento_Slides.bento.html`) bundles the
|
|
5
|
+
following third-party open-source components. Their license terms require that
|
|
6
|
+
these notices accompany copies, so the same text is embedded as a `NOTICE`
|
|
7
|
+
comment near the top of every built shell and every saved Bento document.
|
|
8
|
+
|
|
9
|
+
## Bundled runtime (ships inside the shell)
|
|
10
|
+
|
|
11
|
+
### reveal.js
|
|
12
|
+
- License: MIT
|
|
13
|
+
- Copyright (C) 2011-2024 Hakim El Hattab, http://hakim.se, and reveal.js contributors
|
|
14
|
+
- Project: https://revealjs.com / https://github.com/hakimel/reveal.js
|
|
15
|
+
- Use: powers the present-mode fullscreen slideshow overlay.
|
|
16
|
+
|
|
17
|
+
### Moveable
|
|
18
|
+
- License: MIT
|
|
19
|
+
- Copyright (c) 2019 Daybrush (Younkue Choi)
|
|
20
|
+
- Project: https://github.com/daybrush/moveable
|
|
21
|
+
- Use: on-canvas element manipulation (drag / resize / rotate handles). Pulls in
|
|
22
|
+
the author's supporting modules (`@daybrush/*`, `@scena/*`, `@egjs/*`,
|
|
23
|
+
`@cfcs/*`), all MIT, © Daybrush.
|
|
24
|
+
|
|
25
|
+
### Selecto
|
|
26
|
+
- License: MIT
|
|
27
|
+
- Copyright (c) 2020 Daybrush (Younkue Choi)
|
|
28
|
+
- Project: https://github.com/daybrush/selecto
|
|
29
|
+
- Use: marquee / rubber-band selection on the editing canvas.
|
|
30
|
+
|
|
31
|
+
## Bundled fonts (embedded as document assets in decks that use them)
|
|
32
|
+
|
|
33
|
+
### Fraunces
|
|
34
|
+
- License: SIL Open Font License 1.1
|
|
35
|
+
- Copyright 2020 The Fraunces Project Authors
|
|
36
|
+
- Project: https://github.com/undercasetype/Fraunces
|
|
37
|
+
|
|
38
|
+
### Instrument Sans
|
|
39
|
+
- License: SIL Open Font License 1.1
|
|
40
|
+
- Copyright 2022 The Instrument Sans Project Authors
|
|
41
|
+
- Project: https://github.com/Instrument/instrument-sans
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
Full MIT license text is reproduced in the shell's `NOTICE` comment and in the
|
|
46
|
+
`LICENSE` file. The SIL Open Font License 1.1 text is available at
|
|
47
|
+
https://openfontlicense.org.
|
|
48
|
+
|
|
49
|
+
Dev-only tooling (Vite, TypeScript, `vite-plugin-singlefile`, `qrcode`,
|
|
50
|
+
`@types/*`) is used to build Bento but is **not** bundled into the shipped shell
|
|
51
|
+
and is therefore not listed here.
|