@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,517 @@
|
|
|
1
|
+
/** Bounded original-source graph contracts; relationships never derive from positions. */
|
|
2
|
+
import {
|
|
3
|
+
hashSource,
|
|
4
|
+
scanSource,
|
|
5
|
+
sliceBytes,
|
|
6
|
+
type SourceBlock,
|
|
7
|
+
type SourceDocument,
|
|
8
|
+
SourceError,
|
|
9
|
+
type SourcePatch,
|
|
10
|
+
} from "./mod.ts";
|
|
11
|
+
import {
|
|
12
|
+
type DesignDeclaration,
|
|
13
|
+
type PandocAttr,
|
|
14
|
+
parseDesignAttributes,
|
|
15
|
+
} from "../render/design.ts";
|
|
16
|
+
import { canonical } from "../state/json.ts";
|
|
17
|
+
import { isNativeIdentity } from "./identity-id.ts";
|
|
18
|
+
|
|
19
|
+
export type GraphKind = "diagram" | "timeline";
|
|
20
|
+
export interface GraphChild {
|
|
21
|
+
block: SourceBlock;
|
|
22
|
+
attr: PandocAttr;
|
|
23
|
+
declaration: DesignDeclaration;
|
|
24
|
+
label?: SourceBlock;
|
|
25
|
+
}
|
|
26
|
+
export interface SourceGraph {
|
|
27
|
+
kind: GraphKind;
|
|
28
|
+
wrapper: SourceBlock;
|
|
29
|
+
attr: PandocAttr;
|
|
30
|
+
declaration: DesignDeclaration;
|
|
31
|
+
nodes: GraphChild[];
|
|
32
|
+
edges: (GraphChild & { from: string; to: string })[];
|
|
33
|
+
/** Lexical milestone order or deterministic topological columns. */
|
|
34
|
+
layers: string[][];
|
|
35
|
+
}
|
|
36
|
+
interface AttributeToken {
|
|
37
|
+
raw: string;
|
|
38
|
+
start: number;
|
|
39
|
+
key?: string;
|
|
40
|
+
value?: string;
|
|
41
|
+
valueStart?: number;
|
|
42
|
+
valueEnd?: number;
|
|
43
|
+
}
|
|
44
|
+
function fail(code: string, message: string): never {
|
|
45
|
+
throw new SourceError(code, message);
|
|
46
|
+
}
|
|
47
|
+
function opener(block: SourceBlock) {
|
|
48
|
+
const line = block.raw.split(/\r?\n/)[0];
|
|
49
|
+
const match = /^ {0,3}:{3,}\s*\{([^{}]*)\}[ \t]*$/.exec(line);
|
|
50
|
+
if (!match) {
|
|
51
|
+
fail(
|
|
52
|
+
"UNMAPPED_GRAPH_SOURCE",
|
|
53
|
+
"Use a literal identified fenced-div opener.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const content = match[1], contentStart = line.indexOf("{") + 1;
|
|
57
|
+
const tokens: AttributeToken[] = [];
|
|
58
|
+
const pattern = /(?:[^\s"']|"[^"]*"|'[^']*')+/g;
|
|
59
|
+
for (const match of content.matchAll(pattern)) {
|
|
60
|
+
const raw = match[0], start = match.index!;
|
|
61
|
+
if (/^[#.][A-Za-z][\w:.-]*$/.test(raw)) {
|
|
62
|
+
tokens.push({ raw, start });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const pair =
|
|
66
|
+
/^([A-Za-z][A-Za-z0-9_-]*)=(?:"([^"\\]*)"|'([^'\\]*)'|([^\s"'\\]+))$/
|
|
67
|
+
.exec(raw);
|
|
68
|
+
if (!pair) {
|
|
69
|
+
fail(
|
|
70
|
+
"UNMAPPED_GRAPH_SOURCE",
|
|
71
|
+
"Graph attributes require literal key=value syntax without escapes.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const key = pair[1], value = pair[2] ?? pair[3] ?? pair[4];
|
|
75
|
+
const valueStart = start + key.length + 1 + (pair[4] === undefined ? 1 : 0);
|
|
76
|
+
tokens.push({
|
|
77
|
+
raw,
|
|
78
|
+
start,
|
|
79
|
+
key,
|
|
80
|
+
value,
|
|
81
|
+
valueStart,
|
|
82
|
+
valueEnd: valueStart + value.length,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const attr: PandocAttr = ["", [], []];
|
|
86
|
+
for (const token of tokens) {
|
|
87
|
+
if (token.key) attr[2].push([token.key, token.value!]);
|
|
88
|
+
else if (token.raw[0] === "#") {
|
|
89
|
+
if (attr[0]) {
|
|
90
|
+
fail(
|
|
91
|
+
"UNMAPPED_GRAPH_SOURCE",
|
|
92
|
+
"Graph declarations need one explicit identity.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
attr[0] = token.raw.slice(1);
|
|
96
|
+
} else attr[1].push(token.raw.slice(1));
|
|
97
|
+
}
|
|
98
|
+
return { line, contentStart, tokens, attr };
|
|
99
|
+
}
|
|
100
|
+
export function sourceGraphAttributes(block: SourceBlock): PandocAttr {
|
|
101
|
+
return opener(block).attr;
|
|
102
|
+
}
|
|
103
|
+
function role(block: SourceBlock): string | undefined {
|
|
104
|
+
// Read class tokens only; a value containing '.bento-diagram' is not intent.
|
|
105
|
+
const line = block.raw.split(/\r?\n/)[0];
|
|
106
|
+
const content = /\{([^{}]*)\}[ \t]*$/.exec(line)?.[1];
|
|
107
|
+
return (content?.match(/(?:[^\s"']|"[^"]*"|'[^']*')+/g) ?? [])
|
|
108
|
+
.find((token) =>
|
|
109
|
+
/^\.bento-(diagram|timeline|node|edge|milestone)$/.test(token)
|
|
110
|
+
)?.slice(7);
|
|
111
|
+
}
|
|
112
|
+
function graphFrom(
|
|
113
|
+
original: SourceDocument,
|
|
114
|
+
wrapper: SourceBlock,
|
|
115
|
+
): SourceGraph {
|
|
116
|
+
const attr = sourceGraphAttributes(wrapper),
|
|
117
|
+
declaration = parseDesignAttributes(attr);
|
|
118
|
+
if (
|
|
119
|
+
!wrapper.explicitId || !wrapper.stable || attr[0] !== wrapper.id ||
|
|
120
|
+
wrapper.parentId || !wrapper.slideId ||
|
|
121
|
+
!["diagram", "timeline"].includes(declaration.component ?? "")
|
|
122
|
+
) {
|
|
123
|
+
fail(
|
|
124
|
+
"UNMAPPED_GRAPH_SOURCE",
|
|
125
|
+
"Graphs need an original top-level identified .bento-diagram or .bento-timeline under a slide.",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const kind = declaration.component as GraphKind;
|
|
129
|
+
if (original.diagnostics.length) {
|
|
130
|
+
fail(
|
|
131
|
+
"UNMAPPED_GRAPH_SOURCE",
|
|
132
|
+
"Resolve original source structure and duplicate identities before projecting graphs.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const nodes: GraphChild[] = [], edges: SourceGraph["edges"] = [];
|
|
136
|
+
const children = original.blocks.filter((block) =>
|
|
137
|
+
block.parentId === wrapper.id
|
|
138
|
+
);
|
|
139
|
+
for (const block of children) {
|
|
140
|
+
if (block.kind !== "div" || !block.explicitId || !block.stable) {
|
|
141
|
+
fail(
|
|
142
|
+
"GRAPH_CHILD_STRUCTURE",
|
|
143
|
+
"Every direct graph child must be an identified node, edge or milestone div.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const childAttr = sourceGraphAttributes(block),
|
|
147
|
+
childDeclaration = parseDesignAttributes(childAttr);
|
|
148
|
+
const expected = kind === "timeline" ? ["milestone"] : ["node", "edge"];
|
|
149
|
+
if (
|
|
150
|
+
!expected.includes(childDeclaration.component ?? "") ||
|
|
151
|
+
childAttr[0] !== block.id
|
|
152
|
+
) {
|
|
153
|
+
fail(
|
|
154
|
+
"GRAPH_CHILD_STRUCTURE",
|
|
155
|
+
"Direct child roles must match their original graph component.",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const labels = original.blocks.filter((candidate) =>
|
|
159
|
+
candidate.parentId === block.id
|
|
160
|
+
);
|
|
161
|
+
const edge = childDeclaration.component === "edge";
|
|
162
|
+
if (
|
|
163
|
+
(!edge && labels.length !== 1) || (edge && labels.length > 1) ||
|
|
164
|
+
labels.some((label) =>
|
|
165
|
+
label.kind !== "paragraph" || !label.editable ||
|
|
166
|
+
label.origin !== "authored" || label.id !== block.id + "/paragraph:1"
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
fail(
|
|
170
|
+
labels.some((label) =>
|
|
171
|
+
label.origin === "computed" || label.kind === "chunk"
|
|
172
|
+
)
|
|
173
|
+
? "COMPUTED_GRAPH_LABEL"
|
|
174
|
+
: "GRAPH_LABEL_STRUCTURE",
|
|
175
|
+
"Nodes and milestones require one literal authored paragraph; edges allow zero or one. Executable/dynamic labels are outside this adapter.",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const child: GraphChild = {
|
|
179
|
+
block,
|
|
180
|
+
attr: childAttr,
|
|
181
|
+
declaration: childDeclaration,
|
|
182
|
+
...(labels[0] ? { label: labels[0] } : {}),
|
|
183
|
+
};
|
|
184
|
+
if (edge) {
|
|
185
|
+
const from = childAttr[2].find(([key]) => key === "from")?.[1];
|
|
186
|
+
const to = childAttr[2].find(([key]) => key === "to")?.[1];
|
|
187
|
+
if (!from || !to) {
|
|
188
|
+
fail(
|
|
189
|
+
"GRAPH_ENDPOINT_REQUIRED",
|
|
190
|
+
"Each diagram edge needs literal from and to source node IDs.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
edges.push({ ...child, from, to });
|
|
194
|
+
} else nodes.push(child);
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
nodes.length < 2 || nodes.length > 6 || edges.length > 8 ||
|
|
198
|
+
(kind === "diagram" && !edges.length)
|
|
199
|
+
) {
|
|
200
|
+
fail(
|
|
201
|
+
"GRAPH_ARITY",
|
|
202
|
+
"This adapter supports 2..6 nodes/milestones and diagram edges 1..8.",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const nodeIds = nodes.map((child) => child.block.id),
|
|
206
|
+
pairs = new Set<string>();
|
|
207
|
+
for (const edge of edges) {
|
|
208
|
+
if (
|
|
209
|
+
!nodeIds.includes(edge.from) || !nodeIds.includes(edge.to) ||
|
|
210
|
+
edge.from === edge.to
|
|
211
|
+
) {
|
|
212
|
+
fail(
|
|
213
|
+
"GRAPH_ENDPOINT_SCOPE",
|
|
214
|
+
"Diagram endpoints must identify distinct direct nodes in the same original component.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const pair = canonical([edge.from, edge.to]);
|
|
218
|
+
if (pairs.has(pair)) {
|
|
219
|
+
fail(
|
|
220
|
+
"GRAPH_DUPLICATE_EDGE",
|
|
221
|
+
"Parallel edges need a separate faithful routing adapter.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
pairs.add(pair);
|
|
225
|
+
}
|
|
226
|
+
const layers: string[][] = [];
|
|
227
|
+
if (kind === "timeline") {
|
|
228
|
+
// Lexical source order is semantic; native stacking and positions are visual.
|
|
229
|
+
layers.push(...nodeIds.map((id) => [id]));
|
|
230
|
+
} else {
|
|
231
|
+
const remaining = new Set(nodeIds), depth = new Map<string, number>();
|
|
232
|
+
while (remaining.size) {
|
|
233
|
+
const ready = nodeIds.filter((id) =>
|
|
234
|
+
remaining.has(id) &&
|
|
235
|
+
edges.filter((edge) => edge.to === id).every((edge) =>
|
|
236
|
+
depth.has(edge.from)
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
if (!ready.length) {
|
|
240
|
+
fail(
|
|
241
|
+
"GRAPH_TOPOLOGY_UNSUPPORTED",
|
|
242
|
+
"Cycles require another adapter; this slice supports small directed acyclic graphs.",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
for (const id of ready) {
|
|
246
|
+
const incoming = edges.filter((edge) => edge.to === id);
|
|
247
|
+
const level = incoming.length
|
|
248
|
+
? Math.max(...incoming.map((edge) => depth.get(edge.from)!)) + 1
|
|
249
|
+
: 0;
|
|
250
|
+
(layers[level] ??= []).push(id);
|
|
251
|
+
depth.set(id, level);
|
|
252
|
+
remaining.delete(id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (layers.length > 3 || layers.some((layer) => layer.length > 2)) {
|
|
256
|
+
fail(
|
|
257
|
+
"GRAPH_TOPOLOGY_UNSUPPORTED",
|
|
258
|
+
"Use at most three topological columns and two nodes per column; enlarge a different adapter instead of shrinking this graph.",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { kind, wrapper, attr, declaration, nodes, edges, layers };
|
|
263
|
+
}
|
|
264
|
+
export function sourceGraphs(document: SourceDocument): SourceGraph[] {
|
|
265
|
+
if (hashSource(document.text) !== document.hash) {
|
|
266
|
+
fail("stale-document", "Graph source snapshot changed.");
|
|
267
|
+
}
|
|
268
|
+
const original = scanSource(document.text);
|
|
269
|
+
const wrappers = original.blocks.filter((block) =>
|
|
270
|
+
block.kind === "div" && ["diagram", "timeline"].includes(role(block) ?? "")
|
|
271
|
+
);
|
|
272
|
+
const graphs = wrappers.map((wrapper) => graphFrom(original, wrapper));
|
|
273
|
+
const claimed = new Set(
|
|
274
|
+
graphs.flatMap((graph) => [
|
|
275
|
+
graph.wrapper.id,
|
|
276
|
+
...graph.nodes.map((node) => node.block.id),
|
|
277
|
+
...graph.edges.map((edge) => edge.block.id),
|
|
278
|
+
]),
|
|
279
|
+
);
|
|
280
|
+
if (
|
|
281
|
+
original.blocks.some((block) =>
|
|
282
|
+
block.kind === "div" && role(block) && !claimed.has(block.id)
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
fail(
|
|
286
|
+
"GRAPH_CHILD_SCOPE",
|
|
287
|
+
"Graph node/edge/milestone roles belong only to their matching direct original graph component.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return graphs;
|
|
291
|
+
}
|
|
292
|
+
export function sourceGraph(
|
|
293
|
+
document: SourceDocument,
|
|
294
|
+
componentId: string,
|
|
295
|
+
): SourceGraph {
|
|
296
|
+
const matches = sourceGraphs(document).filter((graph) =>
|
|
297
|
+
graph.wrapper.id === componentId
|
|
298
|
+
);
|
|
299
|
+
if (matches.length !== 1) {
|
|
300
|
+
fail(
|
|
301
|
+
"UNMAPPED_GRAPH_SOURCE",
|
|
302
|
+
"Graph identity does not identify one original component.",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return matches[0];
|
|
306
|
+
}
|
|
307
|
+
/** Flow-sensitive source signature excludes paragraph text and optional slide traces. */
|
|
308
|
+
export function graphFlowSignature(
|
|
309
|
+
document: SourceDocument,
|
|
310
|
+
graph: SourceGraph,
|
|
311
|
+
): string {
|
|
312
|
+
const profile = (block: SourceBlock): unknown => {
|
|
313
|
+
if (block.kind === "paragraph") {
|
|
314
|
+
if (
|
|
315
|
+
block.origin === "computed" || /!\[|!\s*<|\$\$|\\\[/.test(block.content)
|
|
316
|
+
) {
|
|
317
|
+
return ["media-or-dynamic-paragraph", hashSource(block.content)];
|
|
318
|
+
}
|
|
319
|
+
return ["paragraph", 110];
|
|
320
|
+
}
|
|
321
|
+
if (block.kind === "math") {
|
|
322
|
+
return [
|
|
323
|
+
"math",
|
|
324
|
+
Math.min(
|
|
325
|
+
330,
|
|
326
|
+
Math.max(110, (block.content.trim().split(/\r?\n/).length + 1) * 44),
|
|
327
|
+
) + 20,
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
if (block.kind === "code") {
|
|
331
|
+
return [
|
|
332
|
+
"code",
|
|
333
|
+
Math.min(
|
|
334
|
+
470,
|
|
335
|
+
Math.max(
|
|
336
|
+
90,
|
|
337
|
+
(block.content.replace(/\r?\n$/, "").split(/\r?\n/).length + 1) *
|
|
338
|
+
30,
|
|
339
|
+
),
|
|
340
|
+
) + 20,
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
if (block.kind === "heading" && block.level === 2) return ["slide"];
|
|
344
|
+
if (block.kind === "div") {
|
|
345
|
+
const attr = sourceGraphAttributes(block);
|
|
346
|
+
const children = document.blocks.filter((child) =>
|
|
347
|
+
child.parentId === block.id
|
|
348
|
+
);
|
|
349
|
+
const child = children[0];
|
|
350
|
+
// Reviewed paragraph identity acquisition adds no visual layout. Keep its
|
|
351
|
+
// exact native flow equivalent without trusting arbitrary container syntax.
|
|
352
|
+
if (
|
|
353
|
+
!block.parentId && block.explicitId === block.id &&
|
|
354
|
+
!attr[1].length && attr[2].length === 1 &&
|
|
355
|
+
attr[2][0][0] === "bento-element-id" &&
|
|
356
|
+
isNativeIdentity(attr[2][0][1]) && children.length === 1 &&
|
|
357
|
+
child.kind === "paragraph" && child.origin === "authored" &&
|
|
358
|
+
!/!\[|!\s*<|\$\$|\\\[/.test(child.content) &&
|
|
359
|
+
!sliceBytes(document.text, {
|
|
360
|
+
start: block.contentSpan.start,
|
|
361
|
+
end: child.span.start,
|
|
362
|
+
}).trim() &&
|
|
363
|
+
!sliceBytes(document.text, {
|
|
364
|
+
start: child.span.end,
|
|
365
|
+
end: block.contentSpan.end,
|
|
366
|
+
}).trim()
|
|
367
|
+
) return profile(child);
|
|
368
|
+
if (attr[1].includes("notes")) return ["notes"];
|
|
369
|
+
if (
|
|
370
|
+
attr[1].includes("bento-diagram") || attr[1].includes("bento-timeline")
|
|
371
|
+
) {
|
|
372
|
+
return [
|
|
373
|
+
"graph",
|
|
374
|
+
attr[2].filter(([key]) => ["x", "y", "w", "h"].includes(key)),
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
if (!attr[1].some((name) => name.startsWith("bento-"))) {
|
|
378
|
+
return [
|
|
379
|
+
"div",
|
|
380
|
+
attr,
|
|
381
|
+
children.map(profile),
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return [block.kind, hashSource(block.raw)];
|
|
386
|
+
};
|
|
387
|
+
return hashSource(
|
|
388
|
+
canonical(
|
|
389
|
+
document.blocks.filter((block) =>
|
|
390
|
+
block.slideId === graph.wrapper.slideId && !block.parentId &&
|
|
391
|
+
block.span.start < graph.wrapper.span.start
|
|
392
|
+
).map(profile),
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
export const graphNodeId = (id: string) => id + "/qb-node";
|
|
397
|
+
export const graphEdgeId = (id: string) => id + "/qb-edge";
|
|
398
|
+
export const timelineLinkId = (componentId: string, from: string, to: string) =>
|
|
399
|
+
componentId + "/qb-link-" + hashSource(canonical([from, to])).slice(0, 24);
|
|
400
|
+
|
|
401
|
+
export interface GraphEndpointRequest {
|
|
402
|
+
componentId: string;
|
|
403
|
+
edgeId: string;
|
|
404
|
+
endpoint: "from" | "to";
|
|
405
|
+
expected: string;
|
|
406
|
+
nodeId: string;
|
|
407
|
+
}
|
|
408
|
+
export interface GraphEndpointPreview {
|
|
409
|
+
version: 1;
|
|
410
|
+
kind: "graph-endpoints";
|
|
411
|
+
sourceHash: string;
|
|
412
|
+
requests: GraphEndpointRequest[];
|
|
413
|
+
patches: SourcePatch[];
|
|
414
|
+
result: string;
|
|
415
|
+
resultHash: string;
|
|
416
|
+
}
|
|
417
|
+
/** Only exact endpoint value spans are changed, never a Pandoc writer's document. */
|
|
418
|
+
export function previewGraphEndpoints(
|
|
419
|
+
document: SourceDocument,
|
|
420
|
+
requests: GraphEndpointRequest[],
|
|
421
|
+
): GraphEndpointPreview {
|
|
422
|
+
const patches: SourcePatch[] = [], seen = new Set<string>();
|
|
423
|
+
for (const request of requests) {
|
|
424
|
+
const key = canonical([request.edgeId, request.endpoint]);
|
|
425
|
+
if (seen.has(key)) {
|
|
426
|
+
fail("GRAPH_DUPLICATE_PATCH", "Choose each edge endpoint once.");
|
|
427
|
+
}
|
|
428
|
+
seen.add(key);
|
|
429
|
+
const graph = sourceGraph(document, request.componentId);
|
|
430
|
+
const edge = graph.edges.find((edge) => edge.block.id === request.edgeId);
|
|
431
|
+
if (
|
|
432
|
+
graph.kind !== "diagram" || !edge ||
|
|
433
|
+
!["from", "to"].includes(request.endpoint) ||
|
|
434
|
+
!graph.nodes.some((node) => node.block.id === request.nodeId)
|
|
435
|
+
) {
|
|
436
|
+
fail(
|
|
437
|
+
"GRAPH_ENDPOINT_SCOPE",
|
|
438
|
+
"Endpoint patches need an original edge and a direct node of the same diagram.",
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (edge[request.endpoint] !== request.expected) {
|
|
442
|
+
fail(
|
|
443
|
+
"SOURCE_CONFLICT",
|
|
444
|
+
"QMD independently changed this graph endpoint; compare and resolve before syncing.",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const header = opener(edge.block);
|
|
448
|
+
const matches = header.tokens.filter((token) =>
|
|
449
|
+
token.key === request.endpoint
|
|
450
|
+
);
|
|
451
|
+
if (matches.length !== 1) {
|
|
452
|
+
fail(
|
|
453
|
+
"UNMAPPED_GRAPH_SOURCE",
|
|
454
|
+
"Endpoint has no unique exact original value span.",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const token = matches[0],
|
|
458
|
+
bytes = (value: string) => new TextEncoder().encode(value).length;
|
|
459
|
+
patches.push({
|
|
460
|
+
blockId: request.edgeId,
|
|
461
|
+
span: {
|
|
462
|
+
start: edge.block.span.start +
|
|
463
|
+
bytes(header.line.slice(0, header.contentStart + token.valueStart!)),
|
|
464
|
+
end: edge.block.span.start +
|
|
465
|
+
bytes(header.line.slice(0, header.contentStart + token.valueEnd!)),
|
|
466
|
+
},
|
|
467
|
+
expected: request.expected,
|
|
468
|
+
replacement: request.nodeId,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
let result = document.text;
|
|
472
|
+
for (
|
|
473
|
+
const patch of [...patches].sort((a, b) => b.span.start - a.span.start)
|
|
474
|
+
) {
|
|
475
|
+
if (sliceBytes(result, patch.span) !== patch.expected) {
|
|
476
|
+
fail(
|
|
477
|
+
"stale-fragment",
|
|
478
|
+
patch.blockId,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const encoded = new TextEncoder().encode(result);
|
|
482
|
+
result = new TextDecoder("utf-8", { fatal: true }).decode(
|
|
483
|
+
encoded.slice(0, patch.span.start),
|
|
484
|
+
) +
|
|
485
|
+
patch.replacement +
|
|
486
|
+
new TextDecoder("utf-8", { fatal: true }).decode(
|
|
487
|
+
encoded.slice(patch.span.end),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
// Topology/membership validation happens before a review can approve source writes.
|
|
491
|
+
sourceGraphs(scanSource(result));
|
|
492
|
+
return {
|
|
493
|
+
version: 1,
|
|
494
|
+
kind: "graph-endpoints",
|
|
495
|
+
sourceHash: document.hash,
|
|
496
|
+
requests: structuredClone(requests),
|
|
497
|
+
patches,
|
|
498
|
+
result,
|
|
499
|
+
resultHash: hashSource(result),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
export function applyGraphEndpoints(
|
|
503
|
+
text: string,
|
|
504
|
+
plan: GraphEndpointPreview,
|
|
505
|
+
): string {
|
|
506
|
+
if (hashSource(text) !== plan.sourceHash) {
|
|
507
|
+
fail("STALE_SOURCE", "QMD changed since the graph endpoint review.");
|
|
508
|
+
}
|
|
509
|
+
const verified = previewGraphEndpoints(scanSource(text), plan.requests);
|
|
510
|
+
if (canonical(verified) !== canonical(plan)) {
|
|
511
|
+
fail(
|
|
512
|
+
"INVALID_PLAN",
|
|
513
|
+
"Graph endpoint plan was altered or no longer reproduces its exact source result.",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return verified.result;
|
|
517
|
+
}
|