@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,415 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hashSource,
|
|
3
|
+
scanSource,
|
|
4
|
+
type SourceDocument,
|
|
5
|
+
SourceError,
|
|
6
|
+
type SourcePatch,
|
|
7
|
+
} from "./mod.ts";
|
|
8
|
+
import {
|
|
9
|
+
nativeDesignField,
|
|
10
|
+
type PandocAttr,
|
|
11
|
+
parseDesignAttributes,
|
|
12
|
+
validateDesignValue,
|
|
13
|
+
} from "../render/design.ts";
|
|
14
|
+
|
|
15
|
+
export interface AttributeRequest {
|
|
16
|
+
blockId: string;
|
|
17
|
+
attribute: string;
|
|
18
|
+
value: string | number | null;
|
|
19
|
+
expected?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AttributePreview {
|
|
22
|
+
version: 1;
|
|
23
|
+
kind: "visual-attributes";
|
|
24
|
+
sourceHash: string;
|
|
25
|
+
requests: AttributeRequest[];
|
|
26
|
+
patches: SourcePatch[];
|
|
27
|
+
result: string;
|
|
28
|
+
resultHash: string;
|
|
29
|
+
}
|
|
30
|
+
interface Token {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
key?: string;
|
|
34
|
+
value?: string;
|
|
35
|
+
valueStart?: number;
|
|
36
|
+
valueEnd?: number;
|
|
37
|
+
quote?: string;
|
|
38
|
+
}
|
|
39
|
+
const encoder = new TextEncoder(),
|
|
40
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
41
|
+
/** Token offsets are local UTF-16; converted to UTF-8 only at the patch boundary. */
|
|
42
|
+
function tokens(content: string): Token[] {
|
|
43
|
+
const result: Token[] = [];
|
|
44
|
+
let cursor = 0;
|
|
45
|
+
while (cursor < content.length) {
|
|
46
|
+
if (/\s/.test(content[cursor])) {
|
|
47
|
+
cursor++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const start = cursor;
|
|
51
|
+
if (content[cursor] === "#" || content[cursor] === ".") {
|
|
52
|
+
cursor++;
|
|
53
|
+
while (cursor < content.length && !/\s/.test(content[cursor])) cursor++;
|
|
54
|
+
if (
|
|
55
|
+
!/^[#.][\p{L}_][\p{L}\p{N}\p{M}_.:-]*$/u.test(
|
|
56
|
+
content.slice(start, cursor),
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
throw new SourceError(
|
|
60
|
+
"unsupported-attributes",
|
|
61
|
+
"Malformed class/id token.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
result.push({ start, end: cursor });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
while (cursor < content.length && /[A-Za-z0-9_-]/.test(content[cursor])) {
|
|
68
|
+
cursor++;
|
|
69
|
+
}
|
|
70
|
+
const key = content.slice(start, cursor);
|
|
71
|
+
if (!key || content[cursor] !== "=") {
|
|
72
|
+
throw new SourceError(
|
|
73
|
+
"unsupported-attributes",
|
|
74
|
+
"Expected a key=value attribute without spaces around '='.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
cursor++;
|
|
78
|
+
const quote = ["'", '"'].includes(content[cursor])
|
|
79
|
+
? content[cursor++]
|
|
80
|
+
: undefined;
|
|
81
|
+
const valueStart = cursor;
|
|
82
|
+
if (quote) {
|
|
83
|
+
while (cursor < content.length && content[cursor] !== quote) {
|
|
84
|
+
if (content[cursor] === "\\") {
|
|
85
|
+
throw new SourceError(
|
|
86
|
+
"unsupported-attributes",
|
|
87
|
+
"Escaped attribute syntax needs explicit mapping.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
cursor++;
|
|
91
|
+
}
|
|
92
|
+
if (cursor === content.length) {
|
|
93
|
+
throw new SourceError(
|
|
94
|
+
"unsupported-attributes",
|
|
95
|
+
"Unclosed quoted value.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
} else {while (cursor < content.length && !/\s/.test(content[cursor])) {
|
|
99
|
+
cursor++;
|
|
100
|
+
}}
|
|
101
|
+
const valueEnd = cursor, value = content.slice(valueStart, valueEnd);
|
|
102
|
+
if (!value || /[{}\r\n]/.test(value)) {
|
|
103
|
+
throw new SourceError(
|
|
104
|
+
"unsupported-attributes",
|
|
105
|
+
"Empty or nested attribute syntax.",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (quote) cursor++;
|
|
109
|
+
if (cursor < content.length && !/\s/.test(content[cursor])) {
|
|
110
|
+
throw new SourceError(
|
|
111
|
+
"unsupported-attributes",
|
|
112
|
+
"Attribute tokens require whitespace separation.",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
result.push({
|
|
116
|
+
start,
|
|
117
|
+
end: cursor,
|
|
118
|
+
key,
|
|
119
|
+
value,
|
|
120
|
+
valueStart,
|
|
121
|
+
valueEnd,
|
|
122
|
+
quote,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function asAttr(content: string, parsed: Token[]): PandocAttr {
|
|
128
|
+
let id = "";
|
|
129
|
+
const classes: string[] = [], attributes: [string, string][] = [];
|
|
130
|
+
for (const token of parsed) {
|
|
131
|
+
if (token.key) attributes.push([token.key, token.value!]);
|
|
132
|
+
else {
|
|
133
|
+
const value = content.slice(token.start, token.end);
|
|
134
|
+
if (value[0] === "#") {
|
|
135
|
+
if (id) {
|
|
136
|
+
throw new SourceError("unsupported-attributes", "Multiple IDs.");
|
|
137
|
+
}
|
|
138
|
+
id = value.slice(1);
|
|
139
|
+
} else classes.push(value.slice(1));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [id, classes, attributes];
|
|
143
|
+
}
|
|
144
|
+
/** Read exact original values only from supported, stable source declarations. */
|
|
145
|
+
export function sourceDesignAttribute(
|
|
146
|
+
doc: SourceDocument,
|
|
147
|
+
blockId: string,
|
|
148
|
+
attribute: string,
|
|
149
|
+
): string {
|
|
150
|
+
const blocks = doc.blocks.filter((b) => b.id === blockId);
|
|
151
|
+
if (
|
|
152
|
+
blocks.length !== 1 || !blocks[0].stable ||
|
|
153
|
+
!["heading", "div", "code"].includes(blocks[0].kind)
|
|
154
|
+
) {
|
|
155
|
+
throw new SourceError(
|
|
156
|
+
"unmapped",
|
|
157
|
+
"Visual attributes need an identified source declaration.",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const line = blocks[0].raw.split(/\r?\n/)[0];
|
|
161
|
+
const content = /\{([^{}]*)\}[ \t]*$/.exec(line)?.[1];
|
|
162
|
+
if (content === undefined) {
|
|
163
|
+
throw new SourceError("unmapped", "Missing original attribute block.");
|
|
164
|
+
}
|
|
165
|
+
const parsed = tokens(content), attr = asAttr(content, parsed);
|
|
166
|
+
parseDesignAttributes(attr);
|
|
167
|
+
const values = attr[2].filter(([key]) => key === attribute);
|
|
168
|
+
if (values.length !== 1 || !nativeDesignField(attribute)) {
|
|
169
|
+
throw new SourceError(
|
|
170
|
+
"unmapped",
|
|
171
|
+
"Missing or ambiguous original visual attribute.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return values[0][1];
|
|
175
|
+
}
|
|
176
|
+
function splice(text: string, patches: SourcePatch[]): string {
|
|
177
|
+
let bytes = encoder.encode(text);
|
|
178
|
+
for (
|
|
179
|
+
const patch of [...patches].sort((a, b) => b.span.start - a.span.start)
|
|
180
|
+
) {
|
|
181
|
+
const expected = decoder.decode(
|
|
182
|
+
bytes.slice(patch.span.start, patch.span.end),
|
|
183
|
+
);
|
|
184
|
+
if (expected !== patch.expected) {
|
|
185
|
+
throw new SourceError(
|
|
186
|
+
"stale-fragment",
|
|
187
|
+
patch.blockId,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const replacement = encoder.encode(patch.replacement),
|
|
191
|
+
next = new Uint8Array(
|
|
192
|
+
bytes.length - (patch.span.end - patch.span.start) + replacement.length,
|
|
193
|
+
);
|
|
194
|
+
next.set(bytes.slice(0, patch.span.start));
|
|
195
|
+
next.set(replacement, patch.span.start);
|
|
196
|
+
next.set(
|
|
197
|
+
bytes.slice(patch.span.end),
|
|
198
|
+
patch.span.start + replacement.length,
|
|
199
|
+
);
|
|
200
|
+
bytes = next;
|
|
201
|
+
}
|
|
202
|
+
return decoder.decode(bytes);
|
|
203
|
+
}
|
|
204
|
+
/** Review-only bounded patches to explicit visual keys in heading/div/code openers. */
|
|
205
|
+
export function previewAttributePatches(
|
|
206
|
+
document: SourceDocument,
|
|
207
|
+
requests: AttributeRequest[],
|
|
208
|
+
): AttributePreview {
|
|
209
|
+
if (hashSource(document.text) !== document.hash) {
|
|
210
|
+
throw new SourceError("stale-document", "Source snapshot changed.");
|
|
211
|
+
}
|
|
212
|
+
const fresh = scanSource(document.text), patches: SourcePatch[] = [];
|
|
213
|
+
if (fresh.diagnostics.length) {
|
|
214
|
+
throw new SourceError(
|
|
215
|
+
"invalid-source-structure",
|
|
216
|
+
"Resolve scanner diagnostics before patching attributes.",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const grouped = new Map<string, AttributeRequest[]>();
|
|
220
|
+
for (const request of requests) {
|
|
221
|
+
if (!nativeDesignField(request.attribute)) {
|
|
222
|
+
throw new SourceError(
|
|
223
|
+
"unsupported-attribute",
|
|
224
|
+
"Only implemented visual fields are patchable.",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (request.value !== null) {
|
|
228
|
+
validateDesignValue(request.attribute, request.value);
|
|
229
|
+
}
|
|
230
|
+
grouped.set(request.blockId, [
|
|
231
|
+
...(grouped.get(request.blockId) ?? []),
|
|
232
|
+
request,
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
for (const [id, edits] of grouped) {
|
|
236
|
+
const matches = fresh.blocks.filter((b) => b.id === id);
|
|
237
|
+
if (matches.length !== 1 || !matches[0].stable) {
|
|
238
|
+
throw new SourceError(
|
|
239
|
+
"unmapped",
|
|
240
|
+
"Pin a unique source identity before editing visual attributes.",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const block = matches[0];
|
|
244
|
+
if (!["heading", "div", "code"].includes(block.kind)) {
|
|
245
|
+
throw new SourceError(
|
|
246
|
+
"unsupported-attribute-target",
|
|
247
|
+
"Use attributes on an identified heading/div/displayed code; computation options are not visual attributes.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const line = block.raw.split(/\r?\n/)[0];
|
|
251
|
+
const match = /\{([^{}]*)\}[ \t]*$/.exec(line);
|
|
252
|
+
if (!match) {
|
|
253
|
+
throw new SourceError(
|
|
254
|
+
"unmapped",
|
|
255
|
+
"An existing explicit attribute block is required; source syntax was not guessed.",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const content = match[1],
|
|
259
|
+
parsed = tokens(content),
|
|
260
|
+
attr = asAttr(content, parsed);
|
|
261
|
+
parseDesignAttributes(attr); // also rejects ambiguous aliases and malformed declarations
|
|
262
|
+
const attrsStart = match.index + 1, seen = new Set<string>();
|
|
263
|
+
const local: {
|
|
264
|
+
start: number;
|
|
265
|
+
end: number;
|
|
266
|
+
expected: string;
|
|
267
|
+
replacement: string;
|
|
268
|
+
}[] = [];
|
|
269
|
+
for (const edit of edits) {
|
|
270
|
+
const native = nativeDesignField(edit.attribute)!;
|
|
271
|
+
if (seen.has(native)) {
|
|
272
|
+
throw new SourceError(
|
|
273
|
+
"overlapping-patches",
|
|
274
|
+
"Repeated or aliased attribute requests.",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
seen.add(native);
|
|
278
|
+
const entries = parsed.filter((t) => t.key === edit.attribute);
|
|
279
|
+
if (entries.length > 1) {
|
|
280
|
+
throw new SourceError("unmapped", "Duplicate attribute key.");
|
|
281
|
+
}
|
|
282
|
+
const token = entries[0];
|
|
283
|
+
if (edit.expected !== undefined && token?.value !== edit.expected) {
|
|
284
|
+
throw new SourceError("stale-fragment", `${id}:${edit.attribute}`);
|
|
285
|
+
}
|
|
286
|
+
if (edit.value === null) {
|
|
287
|
+
if (!token) {
|
|
288
|
+
throw new SourceError(
|
|
289
|
+
"unmapped",
|
|
290
|
+
"Cannot remove an absent attribute.",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
local.push({
|
|
294
|
+
start: token.start,
|
|
295
|
+
end: token.end,
|
|
296
|
+
expected: content.slice(token.start, token.end),
|
|
297
|
+
replacement: "",
|
|
298
|
+
});
|
|
299
|
+
} else if (token) {
|
|
300
|
+
const value = String(edit.value);
|
|
301
|
+
// Preserve original quotes when possible. Unquoted values that need spaces
|
|
302
|
+
// are quoted as one token rather than introducing new attributes.
|
|
303
|
+
if (!token.quote && /\s/.test(value)) {
|
|
304
|
+
local.push({
|
|
305
|
+
start: token.valueStart!,
|
|
306
|
+
end: token.valueEnd!,
|
|
307
|
+
expected: token.value!,
|
|
308
|
+
replacement: `"${value}"`,
|
|
309
|
+
});
|
|
310
|
+
} else {local.push({
|
|
311
|
+
start: token.valueStart!,
|
|
312
|
+
end: token.valueEnd!,
|
|
313
|
+
expected: token.value!,
|
|
314
|
+
replacement: value,
|
|
315
|
+
});}
|
|
316
|
+
} else {local.push({
|
|
317
|
+
start: content.length,
|
|
318
|
+
end: content.length,
|
|
319
|
+
expected: "",
|
|
320
|
+
replacement: ` ${edit.attribute}="${String(edit.value)}"`,
|
|
321
|
+
});}
|
|
322
|
+
}
|
|
323
|
+
// Coalesce insertions at the same closing brace while keeping other fields surgical.
|
|
324
|
+
const insertions = local.filter((p) => p.start === p.end);
|
|
325
|
+
const changes = local.filter((p) => p.start !== p.end);
|
|
326
|
+
if (insertions.length) {
|
|
327
|
+
changes.push({
|
|
328
|
+
...insertions[0],
|
|
329
|
+
replacement: insertions.map((p) => p.replacement).join(""),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
for (const change of changes) {
|
|
333
|
+
const start = block.span.start +
|
|
334
|
+
encoder.encode(line.slice(0, attrsStart + change.start)).length;
|
|
335
|
+
const end = block.span.start +
|
|
336
|
+
encoder.encode(line.slice(0, attrsStart + change.end)).length;
|
|
337
|
+
patches.push({
|
|
338
|
+
blockId: id,
|
|
339
|
+
span: { start, end },
|
|
340
|
+
expected: change.expected,
|
|
341
|
+
replacement: change.replacement,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
patches.sort((a, b) => a.span.start - b.span.start);
|
|
346
|
+
for (let i = 1; i < patches.length; i++) {
|
|
347
|
+
if (patches[i].span.start < patches[i - 1].span.end) {
|
|
348
|
+
throw new SourceError("overlapping-patches", "Attribute ranges overlap.");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const result = splice(fresh.text, patches), rescanned = scanSource(result);
|
|
352
|
+
const shape = (d: SourceDocument) =>
|
|
353
|
+
d.blocks.map((b) => [b.kind, b.explicitId, b.level, b.origin, b.content]);
|
|
354
|
+
// Div raw content includes nested opener attributes, so compare semantic children
|
|
355
|
+
// and identities, not the div's aggregate text.
|
|
356
|
+
const normalize = (d: SourceDocument) =>
|
|
357
|
+
shape(d).map((row) => row[0] === "div" ? [...row.slice(0, 4)] : row);
|
|
358
|
+
if (
|
|
359
|
+
JSON.stringify(normalize(fresh)) !== JSON.stringify(normalize(rescanned))
|
|
360
|
+
) {
|
|
361
|
+
throw new SourceError(
|
|
362
|
+
"structural-replacement",
|
|
363
|
+
"Attribute edit changed semantics/identity.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
for (const id of grouped.keys()) {
|
|
367
|
+
const opener = rescanned.blocks.find((b) =>
|
|
368
|
+
b.id === id
|
|
369
|
+
)!.raw.split(/\r?\n/)[0];
|
|
370
|
+
const content = /\{([^{}]*)\}[ \t]*$/.exec(opener)?.[1];
|
|
371
|
+
if (content === undefined) {
|
|
372
|
+
throw new SourceError(
|
|
373
|
+
"invalid-preview",
|
|
374
|
+
"Attribute block disappeared.",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
parseDesignAttributes(asAttr(content, tokens(content)));
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
version: 1,
|
|
381
|
+
kind: "visual-attributes",
|
|
382
|
+
sourceHash: fresh.hash,
|
|
383
|
+
requests: structuredClone(requests),
|
|
384
|
+
patches,
|
|
385
|
+
result,
|
|
386
|
+
resultHash: hashSource(result),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** Apply only a regenerated, exactly matching preview to the unchanged source bytes. */
|
|
390
|
+
export function applyAttributePatches(
|
|
391
|
+
current: string,
|
|
392
|
+
preview: AttributePreview,
|
|
393
|
+
): string {
|
|
394
|
+
if (
|
|
395
|
+
preview.version !== 1 || preview.kind !== "visual-attributes" ||
|
|
396
|
+
hashSource(current) !== preview.sourceHash
|
|
397
|
+
) throw new SourceError("stale-source", "Review a new attribute diff.");
|
|
398
|
+
const verified = previewAttributePatches(
|
|
399
|
+
scanSource(current),
|
|
400
|
+
preview.requests,
|
|
401
|
+
);
|
|
402
|
+
if (JSON.stringify(verified) !== JSON.stringify(preview)) {
|
|
403
|
+
throw new SourceError(
|
|
404
|
+
"invalid-preview",
|
|
405
|
+
"Attribute preview was changed after review.",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return verified.result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Shared bounded original-source attribute syntax, with token offsets retained. */
|
|
412
|
+
export {
|
|
413
|
+
asAttr as originalPandocAttributes,
|
|
414
|
+
tokens as originalAttributeTokens,
|
|
415
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/** Original bytes establish chart intent and table/producer provenance. */
|
|
2
|
+
import {
|
|
3
|
+
hashSource,
|
|
4
|
+
scanSource,
|
|
5
|
+
type SourceBlock,
|
|
6
|
+
type SourceDocument,
|
|
7
|
+
} from "./mod.ts";
|
|
8
|
+
import { authoredTableBlock, parsePipeTable, tableProducer } from "./table.ts";
|
|
9
|
+
import { type ChartKind, chartOption } from "../render/chart.ts";
|
|
10
|
+
import type { Element } from "../render/types.ts";
|
|
11
|
+
import { canonical } from "../state/json.ts";
|
|
12
|
+
import { type PandocAttr, parseDesignAttributes } from "../render/design.ts";
|
|
13
|
+
import {
|
|
14
|
+
originalAttributeTokens,
|
|
15
|
+
originalPandocAttributes,
|
|
16
|
+
} from "./attributes.ts";
|
|
17
|
+
function fail(message: string): never {
|
|
18
|
+
throw new Error(`UNMAPPED_CHART_SOURCE: ${message}`);
|
|
19
|
+
}
|
|
20
|
+
export function isSourceChart(block: SourceBlock): boolean {
|
|
21
|
+
const content = /\{([^{}]*)\}[ \t]*$/.exec(block.raw.split(/\r?\n/)[0])?.[1];
|
|
22
|
+
const tokens: string[] = content?.match(/(?:[^\s"']|"[^"]*"|'[^']*')+/g) ??
|
|
23
|
+
[];
|
|
24
|
+
return tokens.includes(".bento-chart");
|
|
25
|
+
}
|
|
26
|
+
export function chartSourceAttributes(block: SourceBlock): PandocAttr {
|
|
27
|
+
const content = /^ {0,3}:{3,}\s*\{([^{}]*)\}\s*$/.exec(
|
|
28
|
+
block.raw.split(/\r?\n/)[0],
|
|
29
|
+
)?.[1];
|
|
30
|
+
if (content === undefined) {
|
|
31
|
+
fail("Chart ancestors require literal fenced-div attributes.");
|
|
32
|
+
}
|
|
33
|
+
return originalPandocAttributes(content, originalAttributeTokens(content));
|
|
34
|
+
}
|
|
35
|
+
export function chartSource(doc: SourceDocument, wrapperId: string) {
|
|
36
|
+
if (doc.hash !== hashSource(doc.text)) {
|
|
37
|
+
fail("The original chart source snapshot is stale.");
|
|
38
|
+
}
|
|
39
|
+
const original = scanSource(doc.text);
|
|
40
|
+
const candidates = original.blocks.filter((b) =>
|
|
41
|
+
b.id === wrapperId && b.kind === "div" && b.stable && b.explicitId
|
|
42
|
+
);
|
|
43
|
+
if (original.diagnostics.length || candidates.length !== 1) {
|
|
44
|
+
fail("Use one uniquely identified original chart wrapper.");
|
|
45
|
+
}
|
|
46
|
+
const wrapper = candidates[0];
|
|
47
|
+
const header = wrapper.raw.split(/\r?\n/)[0];
|
|
48
|
+
const attr = /^\s*:{3,}\s*\{([^{}]*)\}\s*$/.exec(header)?.[1];
|
|
49
|
+
const tokens = attr?.match(/(?:[^\s"']|"[^"]*"|'[^']*')+/g) ?? [];
|
|
50
|
+
const parsed: PandocAttr = ["", [], []];
|
|
51
|
+
for (const token of tokens) {
|
|
52
|
+
if (token.startsWith("#")) {
|
|
53
|
+
if (parsed[0]) fail("Original chart wrapper has more than one identity.");
|
|
54
|
+
parsed[0] = token.slice(1);
|
|
55
|
+
} else if (token.startsWith(".")) parsed[1].push(token.slice(1));
|
|
56
|
+
else {
|
|
57
|
+
const pair =
|
|
58
|
+
/^([A-Za-z][A-Za-z0-9_-]*)=(?:"([^"\\]*)"|'([^'\\]*)'|([^\s"']+))$/
|
|
59
|
+
.exec(token);
|
|
60
|
+
if (!pair) {
|
|
61
|
+
fail(
|
|
62
|
+
"Original chart attributes need supported literal key=value syntax.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
parsed[2].push([pair[1], pair[2] ?? pair[3] ?? pair[4]]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let declared;
|
|
69
|
+
try {
|
|
70
|
+
declared = parseDesignAttributes(parsed);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
fail(`Invalid original component declaration: ${String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
if (parsed[0] !== wrapper.id || declared.component !== "chart") {
|
|
75
|
+
fail(
|
|
76
|
+
"Original chart wrapper identity or component role disagrees with the source map.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (tokens.filter((t) => t === ".bento-chart").length !== 1) {
|
|
80
|
+
fail(
|
|
81
|
+
"Native chart intent must be explicit in the original .bento-chart wrapper.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const kinds = tokens.filter((t) => t.startsWith("bento-chart-kind="));
|
|
85
|
+
const match = kinds.length === 1
|
|
86
|
+
? /^bento-chart-kind=(?:"(bar|line)"|'(bar|line)'|(bar|line))$/.exec(
|
|
87
|
+
kinds[0],
|
|
88
|
+
)
|
|
89
|
+
: null;
|
|
90
|
+
if (!match) {
|
|
91
|
+
fail("Declare exactly one bento-chart-kind=bar or line in original QMD.");
|
|
92
|
+
}
|
|
93
|
+
const kind = (match[1] ?? match[2] ?? match[3]) as ChartKind;
|
|
94
|
+
const children = original.blocks.filter((b) =>
|
|
95
|
+
b.id !== wrapper.id && b.span.start >= wrapper.contentSpan.start &&
|
|
96
|
+
b.span.end <= wrapper.contentSpan.end
|
|
97
|
+
);
|
|
98
|
+
if (children.length !== 1) {
|
|
99
|
+
fail(
|
|
100
|
+
"A chart wrapper must contain one original literal table or one explicitly labelled producer chunk.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const block = children[0].kind === "chunk"
|
|
104
|
+
? tableProducer(original, wrapper.id)
|
|
105
|
+
: authoredTableBlock(original, wrapper.id);
|
|
106
|
+
const ancestors: string[] = [];
|
|
107
|
+
let parentId = wrapper.parentId, groupId: string | undefined;
|
|
108
|
+
while (parentId) {
|
|
109
|
+
const parent = original.blocks.find((candidate) =>
|
|
110
|
+
candidate.id === parentId
|
|
111
|
+
);
|
|
112
|
+
if (
|
|
113
|
+
!parent || ancestors.includes(parentId) || parent.kind !== "div" ||
|
|
114
|
+
!parent.explicitId || parent.origin !== "authored" ||
|
|
115
|
+
parent.slideId !== wrapper.slideId
|
|
116
|
+
) {
|
|
117
|
+
fail(
|
|
118
|
+
"Chart ancestors must be original identified source divs on the same slide, outside execution output.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const component =
|
|
122
|
+
parseDesignAttributes(chartSourceAttributes(parent)).component;
|
|
123
|
+
if (component && !["card", "layout"].includes(component)) {
|
|
124
|
+
fail(
|
|
125
|
+
"Only original cards/layouts or transparent source wrappers may contain a chart.",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (component) groupId = parent.id;
|
|
129
|
+
ancestors.push(parentId);
|
|
130
|
+
parentId = parent.parentId;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
original,
|
|
134
|
+
wrapper,
|
|
135
|
+
attr: parsed,
|
|
136
|
+
ancestors,
|
|
137
|
+
groupId,
|
|
138
|
+
block,
|
|
139
|
+
kind,
|
|
140
|
+
computed: block.kind === "chunk",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function validateAuthoredChart(
|
|
144
|
+
doc: SourceDocument,
|
|
145
|
+
blockId: string,
|
|
146
|
+
element: Element,
|
|
147
|
+
) {
|
|
148
|
+
const original = scanSource(doc.text);
|
|
149
|
+
const blocks = original.blocks.filter((b) => b.id === blockId);
|
|
150
|
+
if (blocks.length !== 1 || !blocks[0].parentId) {
|
|
151
|
+
fail("Chart lacks one original table fragment.");
|
|
152
|
+
}
|
|
153
|
+
const source = chartSource(original, blocks[0].parentId);
|
|
154
|
+
if (
|
|
155
|
+
source.computed || source.block.id !== blockId ||
|
|
156
|
+
element.type !== "chart" || element.preset !== source.kind ||
|
|
157
|
+
canonical(element.option) !==
|
|
158
|
+
canonical(chartOption(parsePipeTable(source.block.content), source.kind))
|
|
159
|
+
) {
|
|
160
|
+
fail(
|
|
161
|
+
"Executed chart labels, values or kind differ from the exact authored table and original declaration.",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return source.block;
|
|
165
|
+
}
|