@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,205 @@
|
|
|
1
|
+
/** Previously verified source roles can supply visual typography, never meaning. */
|
|
2
|
+
import { clone, type ProjectState, targetKey } from "../state/mod.ts";
|
|
3
|
+
import type { SourceDocument } from "../source/mod.ts";
|
|
4
|
+
import { verifiedSourceSlide } from "./source-scope.ts";
|
|
5
|
+
import { StateError } from "../state/types.ts";
|
|
6
|
+
import { kpiSource } from "../source/kpi.ts";
|
|
7
|
+
import { sourceComponentClass } from "../render/components.ts";
|
|
8
|
+
import type { KpiAppearance } from "../render/kpi.ts";
|
|
9
|
+
import type { BentoDoc, Element } from "../render/types.ts";
|
|
10
|
+
export type KpiAppearances = ReadonlyMap<string, KpiAppearance>;
|
|
11
|
+
/** A globally identified source wrapper must have exactly one native receipt. */
|
|
12
|
+
function sourceBackedScope(
|
|
13
|
+
state: ProjectState,
|
|
14
|
+
source: SourceDocument,
|
|
15
|
+
sourceSlideId: string,
|
|
16
|
+
componentId: string,
|
|
17
|
+
): string | undefined {
|
|
18
|
+
const backgroundId = `${componentId}/qb-kpi-background`;
|
|
19
|
+
const groupId = kpiSource(source, componentId).groupId;
|
|
20
|
+
const candidates = Object.entries(state.provenance).flatMap(
|
|
21
|
+
([key, proof]) => {
|
|
22
|
+
let target: unknown;
|
|
23
|
+
try {
|
|
24
|
+
target = JSON.parse(key);
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
!Array.isArray(target) || target.length !== 2 ||
|
|
30
|
+
typeof target[0] !== "string" || !target[0] ||
|
|
31
|
+
target[1] !== backgroundId ||
|
|
32
|
+
proof.origin !== "authored" || proof.semanticId !== componentId ||
|
|
33
|
+
proof.source?.kind !== "div" || proof.source.blockId !== componentId ||
|
|
34
|
+
proof.source.stable !== true
|
|
35
|
+
) return [];
|
|
36
|
+
const baseline = state.generatedBaseline.slides.find((slide) =>
|
|
37
|
+
slide.id === target[0]
|
|
38
|
+
)
|
|
39
|
+
?.elements.find((element) => element.id === backgroundId);
|
|
40
|
+
return baseline?.type === "shape" && baseline.shape === "rect" &&
|
|
41
|
+
baseline.groupId === groupId
|
|
42
|
+
? [target[0]]
|
|
43
|
+
: [];
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
if (candidates.length !== 1) return;
|
|
47
|
+
try {
|
|
48
|
+
const slideId = verifiedSourceSlide(
|
|
49
|
+
source,
|
|
50
|
+
state,
|
|
51
|
+
sourceSlideId,
|
|
52
|
+
candidates[0],
|
|
53
|
+
);
|
|
54
|
+
// A weak heading's receipt is valid only for this exact execution snapshot.
|
|
55
|
+
const heading = source.slides.find((slide) => slide.id === sourceSlideId)!;
|
|
56
|
+
if (
|
|
57
|
+
!heading.stable &&
|
|
58
|
+
state.provenance[targetKey({ slideId, elementId: backgroundId })].source
|
|
59
|
+
?.hash !== source.hash
|
|
60
|
+
) return;
|
|
61
|
+
return slideId;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof StateError && error.code === "UNMAPPED_SOURCE_SLIDE") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function retainedKpiAppearances(
|
|
70
|
+
state: ProjectState | null | undefined,
|
|
71
|
+
source: SourceDocument,
|
|
72
|
+
): KpiAppearances {
|
|
73
|
+
const appearances = new Map<string, KpiAppearance>();
|
|
74
|
+
if (!state) return appearances;
|
|
75
|
+
for (
|
|
76
|
+
const block of source.blocks.filter((block) =>
|
|
77
|
+
block.kind === "div" && block.stable &&
|
|
78
|
+
sourceComponentClass(block.raw) === ".bento-kpi"
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
const binding = kpiSource(source, block.id), sourceSlideId = block.slideId;
|
|
82
|
+
if (!sourceSlideId) continue;
|
|
83
|
+
// Pending roles have explicit membership; source-backed roles require the
|
|
84
|
+
// unique original background receipt before any native slide is considered.
|
|
85
|
+
const intent = state.componentCreations?.find((intent) =>
|
|
86
|
+
intent.kind === "kpi" && intent.id === block.id
|
|
87
|
+
);
|
|
88
|
+
let slideId: string | undefined;
|
|
89
|
+
if (intent) {
|
|
90
|
+
try {
|
|
91
|
+
slideId = verifiedSourceSlide(
|
|
92
|
+
source,
|
|
93
|
+
state,
|
|
94
|
+
sourceSlideId,
|
|
95
|
+
intent.slideId,
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (
|
|
99
|
+
!(error instanceof StateError) ||
|
|
100
|
+
error.code !== "UNMAPPED_SOURCE_SLIDE"
|
|
101
|
+
) throw error;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
slideId = sourceBackedScope(state, source, sourceSlideId, block.id);
|
|
105
|
+
}
|
|
106
|
+
if (!slideId) continue;
|
|
107
|
+
const elements = new Map<string, Element>();
|
|
108
|
+
const backgroundId = `${block.id}/qb-kpi-background`;
|
|
109
|
+
const backgroundProof =
|
|
110
|
+
state.provenance[targetKey({ slideId, elementId: backgroundId })];
|
|
111
|
+
const baselineBackground = state.generatedBaseline.slides.find((slide) =>
|
|
112
|
+
slide.id === slideId
|
|
113
|
+
)
|
|
114
|
+
?.elements.find((element) => element.id === backgroundId);
|
|
115
|
+
const sourceBacked = backgroundProof?.origin === "authored" &&
|
|
116
|
+
backgroundProof.semanticId === block.id &&
|
|
117
|
+
backgroundProof.source?.kind === "div" &&
|
|
118
|
+
backgroundProof.source?.blockId === block.id &&
|
|
119
|
+
baselineBackground?.type === "shape" &&
|
|
120
|
+
baselineBackground.shape === "rect" &&
|
|
121
|
+
baselineBackground.groupId === binding.groupId;
|
|
122
|
+
for (const slot of binding.slots) {
|
|
123
|
+
const nativeId = slot.paragraph.id;
|
|
124
|
+
const member = intent?.members.find((member) =>
|
|
125
|
+
member.nativeId === nativeId &&
|
|
126
|
+
member.sourceId === slot.wrapper.id &&
|
|
127
|
+
member.role === slot.role
|
|
128
|
+
);
|
|
129
|
+
if (member) {
|
|
130
|
+
elements.set(nativeId, clone(member.snapshot));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!sourceBacked) continue;
|
|
134
|
+
const proof =
|
|
135
|
+
state.provenance[targetKey({ slideId, elementId: nativeId })];
|
|
136
|
+
const current = state.emitted.slides.find((slide) => slide.id === slideId)
|
|
137
|
+
?.elements.find((element) => element.id === nativeId);
|
|
138
|
+
if (
|
|
139
|
+
current?.type === "text" &&
|
|
140
|
+
!!proof && ["authored", "computed"].includes(proof.origin) &&
|
|
141
|
+
proof.semanticId === nativeId && proof.source?.kind === "paragraph" &&
|
|
142
|
+
proof.source?.blockId === nativeId &&
|
|
143
|
+
proof.source?.parentId === slot.wrapper.id &&
|
|
144
|
+
proof.source?.siblings === 1 &&
|
|
145
|
+
proof.source?.hash === backgroundProof.source?.hash
|
|
146
|
+
) {
|
|
147
|
+
elements.set(nativeId, clone(current));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (elements.size) {
|
|
151
|
+
const previous = appearances.get(slideId);
|
|
152
|
+
const combined = new Map(previous?.elements ?? []);
|
|
153
|
+
for (const [id, element] of elements) combined.set(id, element);
|
|
154
|
+
appearances.set(slideId, {
|
|
155
|
+
elements: combined,
|
|
156
|
+
resources: state.emitted as unknown as BentoDoc,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return appearances;
|
|
161
|
+
}
|
|
162
|
+
/** Rescue keeps the actual saved boxes as visual overlays, not source baseline fields. */
|
|
163
|
+
export function preserveRescuedKpiGeometry(
|
|
164
|
+
state: ProjectState,
|
|
165
|
+
appearances: KpiAppearances,
|
|
166
|
+
rescues: { slideId: string; elementIds: string[] }[],
|
|
167
|
+
ownership: ProjectState["ownership"],
|
|
168
|
+
): void {
|
|
169
|
+
for (const rescue of rescues) {
|
|
170
|
+
for (const elementId of rescue.elementIds) {
|
|
171
|
+
const native = appearances.get(rescue.slideId)?.elements.get(elementId);
|
|
172
|
+
const current = state.emitted.slides.find((slide) =>
|
|
173
|
+
slide.id === rescue.slideId
|
|
174
|
+
)
|
|
175
|
+
?.elements.find((element) => element.id === elementId);
|
|
176
|
+
if (!native || current?.type !== "text") continue;
|
|
177
|
+
const target = { slideId: rescue.slideId, elementId },
|
|
178
|
+
key = targetKey(target);
|
|
179
|
+
for (const field of ["x", "y", "w", "h"]) {
|
|
180
|
+
if (
|
|
181
|
+
typeof native[field] !== "number" || !Number.isFinite(native[field])
|
|
182
|
+
) continue;
|
|
183
|
+
if (
|
|
184
|
+
ownership[key]?.some((path) =>
|
|
185
|
+
path === field || field.startsWith(path + "/")
|
|
186
|
+
)
|
|
187
|
+
) continue;
|
|
188
|
+
let overlay = state.overrides.find((overlay) =>
|
|
189
|
+
targetKey(overlay.target) === key
|
|
190
|
+
);
|
|
191
|
+
if (!overlay) {
|
|
192
|
+
overlay = { target, fields: [] };
|
|
193
|
+
state.overrides.push(overlay);
|
|
194
|
+
}
|
|
195
|
+
overlay.fields = overlay.fields.filter((item) =>
|
|
196
|
+
item.path.length !== 1 || item.path[0] !== field
|
|
197
|
+
);
|
|
198
|
+
overlay.fields.push({
|
|
199
|
+
path: [field],
|
|
200
|
+
value: { present: true, value: native[field] },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/** Explicit, design-only application of reusable layout definitions to saved objects. */
|
|
2
|
+
import { withProject } from "./project.ts";
|
|
3
|
+
import { hashSource, scanSource } from "../source/mod.ts";
|
|
4
|
+
import { builtinLayouts, type Frame } from "../render/design.ts";
|
|
5
|
+
import {
|
|
6
|
+
assertDesignRegistryUnchanged,
|
|
7
|
+
readDesignRegistry,
|
|
8
|
+
} from "../render/registry.ts";
|
|
9
|
+
import { nativeLayoutFrame } from "../render/native-layout.ts";
|
|
10
|
+
import { assertProjectDocument } from "../render/validate.ts";
|
|
11
|
+
import {
|
|
12
|
+
assertOutputUnchanged,
|
|
13
|
+
captureEdits,
|
|
14
|
+
equal,
|
|
15
|
+
type ProjectState,
|
|
16
|
+
StateError,
|
|
17
|
+
targetKey,
|
|
18
|
+
} from "../state/mod.ts";
|
|
19
|
+
export interface LayoutRequest {
|
|
20
|
+
expectedDocId: string;
|
|
21
|
+
expectedRevision: number;
|
|
22
|
+
expectedSourceHash: string;
|
|
23
|
+
expectedRegistryHash: string | null;
|
|
24
|
+
slideId: string;
|
|
25
|
+
layout: string;
|
|
26
|
+
/** Ordered explicit native IDs per column; no automatic membership matching. */
|
|
27
|
+
columns: string[][];
|
|
28
|
+
}
|
|
29
|
+
function fail(code: string, message: string): never {
|
|
30
|
+
throw new StateError(code, message);
|
|
31
|
+
}
|
|
32
|
+
function requestShape(request: LayoutRequest) {
|
|
33
|
+
if (
|
|
34
|
+
!request || typeof request !== "object" || Array.isArray(request) ||
|
|
35
|
+
Object.keys(request).sort().join(",") !==
|
|
36
|
+
"columns,expectedDocId,expectedRegistryHash,expectedRevision,expectedSourceHash,layout,slideId" ||
|
|
37
|
+
typeof request.expectedDocId !== "string" || !request.expectedDocId ||
|
|
38
|
+
!Number.isSafeInteger(request.expectedRevision) ||
|
|
39
|
+
request.expectedRevision < 0 ||
|
|
40
|
+
typeof request.expectedSourceHash !== "string" ||
|
|
41
|
+
!/^[a-f0-9]{64}$/.test(request.expectedSourceHash) ||
|
|
42
|
+
!(request.expectedRegistryHash === null ||
|
|
43
|
+
typeof request.expectedRegistryHash === "string" &&
|
|
44
|
+
/^[a-f0-9]{64}$/.test(request.expectedRegistryHash)) ||
|
|
45
|
+
typeof request.slideId !== "string" || !request.slideId ||
|
|
46
|
+
typeof request.layout !== "string" ||
|
|
47
|
+
!Array.isArray(request.columns) || request.columns.length < 1 ||
|
|
48
|
+
request.columns.length > 6 ||
|
|
49
|
+
request.columns.some((ids) =>
|
|
50
|
+
!Array.isArray(ids) || !ids.length || ids.length > 200 ||
|
|
51
|
+
ids.some((id) => typeof id !== "string" || !id)
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
fail(
|
|
55
|
+
"INVALID_LAYOUT_REQUEST",
|
|
56
|
+
"Select a saved slide, layout and explicit objects for every column.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function catalog(
|
|
61
|
+
state: ProjectState,
|
|
62
|
+
text: string,
|
|
63
|
+
registry: Awaited<ReturnType<typeof readDesignRegistry>>,
|
|
64
|
+
) {
|
|
65
|
+
const source = scanSource(text),
|
|
66
|
+
headings = new Set(source.slides.map((slide) => slide.headingId));
|
|
67
|
+
// Weak source IDs include offsets. A newer source cannot safely classify the
|
|
68
|
+
// saved objects (especially slide titles) before a fresh projection.
|
|
69
|
+
if (
|
|
70
|
+
Object.values(state.provenance).some((proof) =>
|
|
71
|
+
typeof proof.source?.hash === "string" &&
|
|
72
|
+
proof.source.hash !== source.hash
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
fail(
|
|
76
|
+
"STALE_SOURCE",
|
|
77
|
+
"QMD changed since the saved projection. Recalculate before applying a layout; saved and pending edits remain recoverable.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const size = state.emitted.size as { width: number; height: number };
|
|
81
|
+
// Existing projected title region is reserved. Explicitly moved source titles
|
|
82
|
+
// can increase it, but are never moved or selected by this action.
|
|
83
|
+
const slides = state.emitted.slides.map((slide) => {
|
|
84
|
+
const titles = slide.elements.filter((element) =>
|
|
85
|
+
element.role === "title" ||
|
|
86
|
+
headings.has(
|
|
87
|
+
String(
|
|
88
|
+
state
|
|
89
|
+
.provenance[targetKey({ slideId: slide.id, elementId: element.id })]
|
|
90
|
+
?.semanticId,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
const top = Math.max(
|
|
95
|
+
size.height * 166 / 720,
|
|
96
|
+
...titles.map((e) => Number(e.y) + Number(e.h) + 24),
|
|
97
|
+
);
|
|
98
|
+
const frame: Frame = {
|
|
99
|
+
x: size.width / 16,
|
|
100
|
+
y: top,
|
|
101
|
+
w: size.width * 7 / 8,
|
|
102
|
+
h: size.height - top - size.height * 84 / 720,
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
id: slide.id,
|
|
106
|
+
label: String(slide.title ?? slide.id),
|
|
107
|
+
frame,
|
|
108
|
+
elements: slide.elements.filter((e) =>
|
|
109
|
+
!titles.includes(e) &&
|
|
110
|
+
["text", "code", "image", "table", "chart"].includes(e.type)
|
|
111
|
+
).map((element) => ({
|
|
112
|
+
id: element.id,
|
|
113
|
+
type: element.type,
|
|
114
|
+
label: String(
|
|
115
|
+
element.html ?? element.content ?? element.alt ?? element.id,
|
|
116
|
+
).replace(/<[^>]*>/g, "").slice(0, 120),
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
docId: state.docId,
|
|
122
|
+
revision: state.revision,
|
|
123
|
+
sourceHash: source.hash,
|
|
124
|
+
registryHash: registry.hash,
|
|
125
|
+
layouts: { ...builtinLayouts, ...registry.registry.layouts },
|
|
126
|
+
slides,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function readLayouts(source: string) {
|
|
130
|
+
return await withProject(source, async (paths, store) => {
|
|
131
|
+
const state = await store.load();
|
|
132
|
+
if (!state) fail("NO_BASELINE", "Render the project first.");
|
|
133
|
+
return catalog(
|
|
134
|
+
state,
|
|
135
|
+
await Deno.readTextFile(paths.source),
|
|
136
|
+
await readDesignRegistry(paths.directory),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function operation(
|
|
141
|
+
source: string,
|
|
142
|
+
request: LayoutRequest,
|
|
143
|
+
save: boolean,
|
|
144
|
+
) {
|
|
145
|
+
requestShape(request);
|
|
146
|
+
return await withProject(source, async (paths, store) => {
|
|
147
|
+
const state = await store.load();
|
|
148
|
+
if (!state) fail("NO_BASELINE", "Render the project first.");
|
|
149
|
+
if (state.docId !== request.expectedDocId) {
|
|
150
|
+
fail("DOC_ID_CHANGED", "Reload the project before applying a layout.");
|
|
151
|
+
}
|
|
152
|
+
if (state.revision !== request.expectedRevision) {
|
|
153
|
+
fail("STALE_REVISION", "Saved objects changed; reopen Apply layout.");
|
|
154
|
+
}
|
|
155
|
+
const text = await Deno.readTextFile(paths.source);
|
|
156
|
+
if (hashSource(text) !== request.expectedSourceHash) {
|
|
157
|
+
fail("STALE_SOURCE", "QMD changed; reopen Apply layout.");
|
|
158
|
+
}
|
|
159
|
+
const registry = await readDesignRegistry(paths.directory);
|
|
160
|
+
if (registry.hash !== request.expectedRegistryHash) {
|
|
161
|
+
fail(
|
|
162
|
+
"DESIGN_REGISTRY_CHANGED",
|
|
163
|
+
"Layout definitions changed; reopen Apply layout.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const choices = catalog(state, text, registry),
|
|
167
|
+
slide = choices.slides.find((s) => s.id === request.slideId);
|
|
168
|
+
if (!slide) fail("LAYOUT_SLIDE_SCOPE", "Choose an existing saved slide.");
|
|
169
|
+
if (!Object.hasOwn(choices.layouts, request.layout)) {
|
|
170
|
+
fail("UNKNOWN_LAYOUT", "Choose a declared layout.");
|
|
171
|
+
}
|
|
172
|
+
const layout = choices.layouts[request.layout];
|
|
173
|
+
if (request.columns.length !== layout.columns) {
|
|
174
|
+
fail(
|
|
175
|
+
"LAYOUT_COLUMN_COUNT",
|
|
176
|
+
"Assign objects explicitly to every column of this layout.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const ids = request.columns.flat();
|
|
180
|
+
if (
|
|
181
|
+
new Set(ids).size !== ids.length ||
|
|
182
|
+
ids.some((id) => !slide.elements.some((e) => e.id === id))
|
|
183
|
+
) {
|
|
184
|
+
fail(
|
|
185
|
+
"LAYOUT_ELEMENT_SCOPE",
|
|
186
|
+
"Every selected object must occur once on this slide; titles stay outside the content layout.",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const width =
|
|
190
|
+
(slide.frame.w - 2 * layout.padding - (layout.columns - 1) * layout.gap) /
|
|
191
|
+
layout.columns;
|
|
192
|
+
const height = slide.frame.h - 2 * layout.padding;
|
|
193
|
+
const doc = structuredClone(state.emitted),
|
|
194
|
+
native = doc.slides.find((s) => s.id === slide.id)!;
|
|
195
|
+
const placements = request.columns.flatMap((members, column) => {
|
|
196
|
+
const h = (height - (members.length - 1) * layout.gap) / members.length;
|
|
197
|
+
if (width <= 0 || h <= 0) {
|
|
198
|
+
fail(
|
|
199
|
+
"LAYOUT_CONTENT_OVERFLOW",
|
|
200
|
+
"The selected layout has no usable content area.",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return members.map((id, row) => {
|
|
204
|
+
const element = native.elements.find((e) => e.id === id)!;
|
|
205
|
+
const frame = nativeLayoutFrame(element, {
|
|
206
|
+
x: slide.frame.x + layout.padding + column * (width + layout.gap),
|
|
207
|
+
y: slide.frame.y + layout.padding + row * (h + layout.gap),
|
|
208
|
+
w: width,
|
|
209
|
+
h,
|
|
210
|
+
});
|
|
211
|
+
Object.assign(element, frame);
|
|
212
|
+
return { id, ...frame };
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
assertProjectDocument(doc);
|
|
216
|
+
const next = equal(doc, state.emitted) ? state : captureEdits(state, doc);
|
|
217
|
+
const preview = {
|
|
218
|
+
...choices,
|
|
219
|
+
slideId: slide.id,
|
|
220
|
+
layout: request.layout,
|
|
221
|
+
placements,
|
|
222
|
+
pending: next.pending.length,
|
|
223
|
+
};
|
|
224
|
+
// Recheck all external authorities after adaptation and before the single
|
|
225
|
+
// state write. Generated HTML is never replaced by this action.
|
|
226
|
+
await assertOutputUnchanged(state, paths.output);
|
|
227
|
+
if (
|
|
228
|
+
hashSource(await Deno.readTextFile(paths.source)) !==
|
|
229
|
+
request.expectedSourceHash
|
|
230
|
+
) fail("STALE_SOURCE", "QMD changed while preparing layout.");
|
|
231
|
+
await assertDesignRegistryUnchanged(
|
|
232
|
+
paths.directory,
|
|
233
|
+
request.expectedRegistryHash,
|
|
234
|
+
);
|
|
235
|
+
if (!save) return preview;
|
|
236
|
+
const saved = next === state ? state : await store.save(next);
|
|
237
|
+
return { ...preview, revision: saved.revision };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
export async function previewLayout(source: string, request: LayoutRequest) {
|
|
241
|
+
return await operation(source, request, false);
|
|
242
|
+
}
|
|
243
|
+
export async function applyLayout(source: string, request: LayoutRequest) {
|
|
244
|
+
return await operation(source, request, true);
|
|
245
|
+
}
|