@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,354 @@
|
|
|
1
|
+
import { producerOutputChildren } from "../render/producer-capture.ts";
|
|
2
|
+
import { canonical } from "../state/json.ts";
|
|
3
|
+
/** Bounded, data-only adapter for executed R Plotly htmlwidgets. */
|
|
4
|
+
import { hashSource, scanSource, type SourceDocument } from "../source/mod.ts";
|
|
5
|
+
import type { Ast, Node } from "../render/types.ts";
|
|
6
|
+
|
|
7
|
+
type ObjectValue = Record<string, unknown>;
|
|
8
|
+
export interface PlotlyCandidate {
|
|
9
|
+
version: 1;
|
|
10
|
+
kind: "plotly";
|
|
11
|
+
source: {
|
|
12
|
+
sourceHash: string;
|
|
13
|
+
sourceId: string;
|
|
14
|
+
producerId: string;
|
|
15
|
+
slideId: string;
|
|
16
|
+
chunkHash: string;
|
|
17
|
+
outputHash: string;
|
|
18
|
+
};
|
|
19
|
+
spec: { data: ObjectValue[]; layout: ObjectValue; config: ObjectValue };
|
|
20
|
+
specHash: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
export function fail(message: string): never {
|
|
25
|
+
throw new Error(`UNSUPPORTED_WIDGET: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
function object(value: unknown, label: string): ObjectValue {
|
|
28
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
29
|
+
fail(`${label} must be an object`);
|
|
30
|
+
}
|
|
31
|
+
return value as ObjectValue;
|
|
32
|
+
}
|
|
33
|
+
function keys(value: ObjectValue, allowed: string, label: string) {
|
|
34
|
+
const set = new Set(allowed.split(" "));
|
|
35
|
+
for (const key of Object.keys(value)) {
|
|
36
|
+
if (!set.has(key)) fail(`${label}.${key} has no verified adapter`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function finiteTree(value: unknown, depth = 0): void {
|
|
40
|
+
if (depth > 24) fail("payload nesting exceeds 24 levels");
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
if (
|
|
43
|
+
value.length > 100_000 ||
|
|
44
|
+
[...value].some((c) => c.charCodeAt(0) < 32 && !"\t\n\r".includes(c))
|
|
45
|
+
) {
|
|
46
|
+
fail("invalid or oversized string");
|
|
47
|
+
}
|
|
48
|
+
} else if (typeof value === "number") {
|
|
49
|
+
if (!Number.isFinite(value)) fail("non-finite number");
|
|
50
|
+
} else if (Array.isArray(value)) {
|
|
51
|
+
if (value.length > 10_000) fail("array exceeds 10000 items");
|
|
52
|
+
for (const item of value) finiteTree(item, depth + 1);
|
|
53
|
+
} else if (value && typeof value === "object") {
|
|
54
|
+
for (const [key, item] of Object.entries(value)) {
|
|
55
|
+
if (["__proto__", "constructor", "prototype"].includes(key)) {
|
|
56
|
+
fail("unsafe object key");
|
|
57
|
+
}
|
|
58
|
+
finiteTree(item, depth + 1);
|
|
59
|
+
}
|
|
60
|
+
} else if (value !== null && typeof value !== "boolean") {
|
|
61
|
+
fail("non-JSON value");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function plainText(value: unknown, label: string) {
|
|
65
|
+
if (typeof value !== "string" || /[<>$]/.test(value)) {
|
|
66
|
+
fail(`${label} requires plain text (HTML and MathJax are not bundled)`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function font(value: unknown) {
|
|
70
|
+
const v = object(value, "font");
|
|
71
|
+
keys(v, "family size color", "font");
|
|
72
|
+
if (v.family !== undefined) plainText(v.family, "font.family");
|
|
73
|
+
}
|
|
74
|
+
function title(value: unknown) {
|
|
75
|
+
if (Array.isArray(value) && value.length === 0) return;
|
|
76
|
+
if (typeof value === "string") return plainText(value, "title");
|
|
77
|
+
const v = object(value, "title");
|
|
78
|
+
keys(
|
|
79
|
+
v,
|
|
80
|
+
"text font standoff x y xanchor yanchor xref yref pad automargin",
|
|
81
|
+
"title",
|
|
82
|
+
);
|
|
83
|
+
if (v.text !== undefined) plainText(v.text, "title.text");
|
|
84
|
+
if (v.font) font(v.font);
|
|
85
|
+
}
|
|
86
|
+
function axis(value: unknown) {
|
|
87
|
+
const v = object(value, "axis");
|
|
88
|
+
keys(
|
|
89
|
+
v,
|
|
90
|
+
"title domain range autorange automargin type tickmode tickvals ticktext dtick tick0 ticks tickformat tickangle tickfont showgrid gridcolor gridwidth zeroline zerolinecolor zerolinewidth showline linecolor linewidth mirror fixedrange side anchor rangemode exponentformat showexponent categoryorder categoryarray visible",
|
|
91
|
+
"axis",
|
|
92
|
+
);
|
|
93
|
+
if (v.title !== undefined) title(v.title);
|
|
94
|
+
if (v.tickfont) font(v.tickfont);
|
|
95
|
+
for (const key of ["ticktext", "categoryarray"]) {
|
|
96
|
+
if (v[key] !== undefined) {
|
|
97
|
+
if (!Array.isArray(v[key])) fail(`axis.${key} must be an array`);
|
|
98
|
+
for (const text of v[key] as unknown[]) plainText(text, `axis.${key}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function trace(value: unknown): ObjectValue {
|
|
103
|
+
const v = object(value, "trace");
|
|
104
|
+
keys(
|
|
105
|
+
v,
|
|
106
|
+
"type x y mode name marker line error_x error_y xaxis yaxis frame text textposition textfont hovertext hoverinfo hovertemplate opacity visible showlegend legendgroup orientation width base offset fill fillcolor connectgaps cliponaxis",
|
|
107
|
+
"trace",
|
|
108
|
+
);
|
|
109
|
+
if (!["scatter", "bar"].includes(v.type as string)) {
|
|
110
|
+
fail("only SVG scatter and bar traces are supported");
|
|
111
|
+
}
|
|
112
|
+
if (v.frame !== undefined && v.frame !== null) {
|
|
113
|
+
fail("animated frames need a separate adapter");
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
v.xaxis !== undefined && v.xaxis !== "x" ||
|
|
117
|
+
v.yaxis !== undefined && v.yaxis !== "y"
|
|
118
|
+
) fail("multiple axes are not yet supported");
|
|
119
|
+
if (
|
|
120
|
+
!Array.isArray(v.x) || !Array.isArray(v.y) || v.x.length !== v.y.length ||
|
|
121
|
+
!v.x.length
|
|
122
|
+
) fail("x and y must be equal nonempty arrays");
|
|
123
|
+
for (const key of ["x", "y"]) {
|
|
124
|
+
for (const item of v[key] as unknown[]) {
|
|
125
|
+
if (typeof item === "string") plainText(item, key);
|
|
126
|
+
else if (typeof item !== "number" && item !== null) {
|
|
127
|
+
fail(`${key} values must be numeric, plain strings or null`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const key of ["name", "text", "hovertext", "hovertemplate"]) {
|
|
132
|
+
if (v[key] !== undefined) {
|
|
133
|
+
for (
|
|
134
|
+
const item of Array.isArray(v[key]) ? v[key] as unknown[] : [v[key]]
|
|
135
|
+
) plainText(item, key);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (v.textfont) font(v.textfont);
|
|
139
|
+
if (v.line) {
|
|
140
|
+
keys(
|
|
141
|
+
object(v.line, "line"),
|
|
142
|
+
"color width dash shape smoothing simplify",
|
|
143
|
+
"line",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (v.marker) {
|
|
147
|
+
const m = object(v.marker, "marker");
|
|
148
|
+
keys(m, "color size symbol opacity line", "marker");
|
|
149
|
+
if (m.line) {
|
|
150
|
+
keys(object(m.line, "marker.line"), "color width", "marker.line");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const key of ["error_x", "error_y"]) {
|
|
154
|
+
if (v[key]) {
|
|
155
|
+
keys(
|
|
156
|
+
object(v[key], key),
|
|
157
|
+
"type array arrayminus symmetric value valueminus visible color thickness width",
|
|
158
|
+
key,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const result = structuredClone(v);
|
|
163
|
+
delete result.frame; // R Plotly's null frame marker is not a Plotly.js trace property.
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/** Does not evaluate htmlwidgets evals/hooks or reuse their executable binding. */
|
|
167
|
+
export function parsePlotlyPayload(payload: string): PlotlyCandidate["spec"] {
|
|
168
|
+
if (payload.length > 2_000_000) fail("payload exceeds 2 MB");
|
|
169
|
+
let parsed: unknown;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(payload);
|
|
172
|
+
} catch {
|
|
173
|
+
fail("malformed widget JSON");
|
|
174
|
+
}
|
|
175
|
+
finiteTree(parsed);
|
|
176
|
+
const root = object(parsed, "htmlwidget");
|
|
177
|
+
keys(root, "x evals jsHooks", "htmlwidget");
|
|
178
|
+
for (const key of ["evals", "jsHooks"]) {
|
|
179
|
+
if (!Array.isArray(root[key]) || root[key].length !== 0) {
|
|
180
|
+
fail(`${key} must be empty`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const x = object(root.x, "plotly");
|
|
184
|
+
keys(
|
|
185
|
+
x,
|
|
186
|
+
"visdat cur_data attrs layout source config data highlight shinyEvents base_url",
|
|
187
|
+
"plotly",
|
|
188
|
+
);
|
|
189
|
+
if (!Array.isArray(x.data) || !x.data.length || x.data.length > 32) {
|
|
190
|
+
fail("requires 1 to 32 traces");
|
|
191
|
+
}
|
|
192
|
+
const data = x.data.map(trace);
|
|
193
|
+
const layout = structuredClone(object(x.layout, "layout"));
|
|
194
|
+
keys(
|
|
195
|
+
layout,
|
|
196
|
+
"title xaxis yaxis margin hovermode showlegend legend font paper_bgcolor plot_bgcolor barmode bargap bargroupgap dragmode",
|
|
197
|
+
"layout",
|
|
198
|
+
);
|
|
199
|
+
if (layout.title !== undefined) title(layout.title);
|
|
200
|
+
for (const key of ["xaxis", "yaxis"]) {
|
|
201
|
+
if (layout[key]) {
|
|
202
|
+
axis(layout[key]);
|
|
203
|
+
const a = layout[key] as ObjectValue;
|
|
204
|
+
if (Array.isArray(a.title) && a.title.length === 0) delete a.title;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (layout.font) font(layout.font);
|
|
208
|
+
if (layout.margin) {
|
|
209
|
+
keys(object(layout.margin, "margin"), "l r b t pad autoexpand", "margin");
|
|
210
|
+
}
|
|
211
|
+
if (layout.legend) {
|
|
212
|
+
keys(
|
|
213
|
+
object(layout.legend, "legend"),
|
|
214
|
+
"x y xanchor yanchor orientation bgcolor bordercolor borderwidth font traceorder",
|
|
215
|
+
"legend",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const config = structuredClone(object(x.config, "config"));
|
|
219
|
+
keys(
|
|
220
|
+
config,
|
|
221
|
+
"modeBarButtonsToAdd showSendToCloud displayModeBar displaylogo scrollZoom doubleClick",
|
|
222
|
+
"config",
|
|
223
|
+
);
|
|
224
|
+
if (config.showSendToCloud !== false) fail("cloud links must be disabled");
|
|
225
|
+
if (
|
|
226
|
+
config.modeBarButtonsToAdd !== undefined &&
|
|
227
|
+
(!Array.isArray(config.modeBarButtonsToAdd) ||
|
|
228
|
+
config.modeBarButtonsToAdd.some((v) =>
|
|
229
|
+
!["hoverclosest", "hovercompare"].includes(v)
|
|
230
|
+
))
|
|
231
|
+
) fail("unsupported modebar buttons");
|
|
232
|
+
if (x.highlight) {
|
|
233
|
+
const h = object(x.highlight, "highlight");
|
|
234
|
+
keys(
|
|
235
|
+
h,
|
|
236
|
+
"on persistent dynamic selectize opacityDim selected debounce",
|
|
237
|
+
"highlight",
|
|
238
|
+
);
|
|
239
|
+
if (
|
|
240
|
+
h.on !== "plotly_click" || h.persistent !== false ||
|
|
241
|
+
h.dynamic !== false || h.selectize !== false || h.debounce !== 0
|
|
242
|
+
) fail("custom htmlwidgets highlighting is not supported");
|
|
243
|
+
}
|
|
244
|
+
// No Shiny/crosstalk binding is attached. Trace keys/customdata are refused above.
|
|
245
|
+
config.displaylogo = false;
|
|
246
|
+
config.responsive = false;
|
|
247
|
+
config.showSendToCloud = false;
|
|
248
|
+
config.modeBarButtonsToRemove = ["toImage", "sendDataToCloud"];
|
|
249
|
+
config.plotGlPixelRatio = 1;
|
|
250
|
+
return { data, layout, config };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Recover a producer from one ORIGINAL identified wrapper and its only chunk. */
|
|
254
|
+
export function collectPlotlyCandidates(
|
|
255
|
+
ast: Ast,
|
|
256
|
+
source: SourceDocument,
|
|
257
|
+
): PlotlyCandidate[] {
|
|
258
|
+
if (hashSource(source.text) !== source.hash) fail("source snapshot is stale");
|
|
259
|
+
source = scanSource(source.text);
|
|
260
|
+
if (source.diagnostics.length) {
|
|
261
|
+
fail("source mapping diagnostics must be resolved first");
|
|
262
|
+
}
|
|
263
|
+
const ids = new Map<string, Node[]>();
|
|
264
|
+
const visit = (nodes: Node[]) => {
|
|
265
|
+
for (const node of nodes) {
|
|
266
|
+
if (node.t === "Div") {
|
|
267
|
+
const [attrs, children] = node.c as [
|
|
268
|
+
[string, string[], unknown[]],
|
|
269
|
+
Node[],
|
|
270
|
+
];
|
|
271
|
+
if (attrs[0]) ids.set(attrs[0], [...ids.get(attrs[0]) ?? [], node]);
|
|
272
|
+
visit(children);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
visit(ast.blocks);
|
|
277
|
+
const result: PlotlyCandidate[] = [];
|
|
278
|
+
for (
|
|
279
|
+
const wrapper of source.blocks.filter((b) => b.kind === "div" && b.stable)
|
|
280
|
+
) {
|
|
281
|
+
const nodes = ids.get(wrapper.id) ?? [];
|
|
282
|
+
if (!nodes.some((n) => JSON.stringify(n).includes("plotly html-widget"))) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (nodes.length !== 1) fail(`ambiguous executed wrapper ${wrapper.id}`);
|
|
286
|
+
const children = source.blocks.filter((b) => b.parentId === wrapper.id);
|
|
287
|
+
if (
|
|
288
|
+
children.length !== 1 || children[0].kind !== "chunk" ||
|
|
289
|
+
!children[0].stable || !children[0].slideId
|
|
290
|
+
) {
|
|
291
|
+
fail(
|
|
292
|
+
`wrapper ${wrapper.id} must contain exactly one labeled original executable chunk`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const chunk = children[0];
|
|
296
|
+
if (/results\s*[:=]\s*["']?asis|asis_output\s*\(/i.test(chunk.raw)) {
|
|
297
|
+
fail("explicit asis output cannot establish widget provenance");
|
|
298
|
+
}
|
|
299
|
+
let current = producerOutputChildren(
|
|
300
|
+
ast,
|
|
301
|
+
(nodes[0].c as [unknown, Node[]])[1],
|
|
302
|
+
source.hash,
|
|
303
|
+
chunk.id,
|
|
304
|
+
);
|
|
305
|
+
for (const className of ["cell", "cell-output-display"]) {
|
|
306
|
+
if (current.length !== 1 || current[0].t !== "Div") {
|
|
307
|
+
fail(
|
|
308
|
+
"widget output requires one cell and one display block; hide echoed source and messages",
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const [attrs, nested] = current[0].c as [
|
|
312
|
+
[string, string[], unknown[]],
|
|
313
|
+
Node[],
|
|
314
|
+
];
|
|
315
|
+
if (attrs[1].length !== 1 || attrs[1][0] !== className) {
|
|
316
|
+
fail("unsupported widget output container");
|
|
317
|
+
}
|
|
318
|
+
current = nested;
|
|
319
|
+
}
|
|
320
|
+
if (current.length !== 1 || current[0].t !== "RawBlock") {
|
|
321
|
+
fail("expected one widget HTML output");
|
|
322
|
+
}
|
|
323
|
+
const [format, html] = current[0].c as [string, string];
|
|
324
|
+
if (format !== "html") fail("widget requires HTML output");
|
|
325
|
+
const matched = html.match(
|
|
326
|
+
/^<div class="plotly html-widget html-fill-item" id="(htmlwidget-[a-f0-9]+)" style="width:100%;height:(\d+)px;"><\/div>\s*<script type="application\/json" data-for="\1">([\s\S]*)<\/script>\s*$/,
|
|
327
|
+
);
|
|
328
|
+
if (!matched) {
|
|
329
|
+
fail("HTML does not match the supported Plotly htmlwidget envelope");
|
|
330
|
+
}
|
|
331
|
+
const height = Number(matched[2]);
|
|
332
|
+
if (height < 100 || height > 1600) {
|
|
333
|
+
fail("widget height must be between 100 and 1600 pixels");
|
|
334
|
+
}
|
|
335
|
+
const spec = parsePlotlyPayload(matched[3]);
|
|
336
|
+
result.push({
|
|
337
|
+
version: 1,
|
|
338
|
+
kind: "plotly",
|
|
339
|
+
source: {
|
|
340
|
+
sourceHash: source.hash,
|
|
341
|
+
sourceId: wrapper.id,
|
|
342
|
+
producerId: chunk.id,
|
|
343
|
+
slideId: chunk.slideId!,
|
|
344
|
+
chunkHash: hashSource(chunk.raw),
|
|
345
|
+
outputHash: hashSource(html),
|
|
346
|
+
},
|
|
347
|
+
spec,
|
|
348
|
+
specHash: hashSource(canonical(spec)),
|
|
349
|
+
width: 800,
|
|
350
|
+
height,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|