@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,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hashSource,
|
|
3
|
+
scanSource,
|
|
4
|
+
type SourceDocument,
|
|
5
|
+
SourceError,
|
|
6
|
+
type SourcePatch,
|
|
7
|
+
} from "./mod.ts";
|
|
8
|
+
import {
|
|
9
|
+
originalAttributeTokens,
|
|
10
|
+
originalPandocAttributes,
|
|
11
|
+
} from "./attributes.ts";
|
|
12
|
+
/** Exact built-in selector IDs at Bento pin 3d6f7f1fcdf2308e875345ce4f1e0ec0820c914b; no inferred aliases. */
|
|
13
|
+
export const DISPLAYED_CODE_LANGUAGES = [
|
|
14
|
+
"js",
|
|
15
|
+
"ts",
|
|
16
|
+
"py",
|
|
17
|
+
"rust",
|
|
18
|
+
"go",
|
|
19
|
+
"java",
|
|
20
|
+
"sh",
|
|
21
|
+
"sql",
|
|
22
|
+
"c",
|
|
23
|
+
"cpp",
|
|
24
|
+
"csharp",
|
|
25
|
+
"kotlin",
|
|
26
|
+
"swift",
|
|
27
|
+
"scala",
|
|
28
|
+
"dart",
|
|
29
|
+
"zig",
|
|
30
|
+
"php",
|
|
31
|
+
"ruby",
|
|
32
|
+
"perl",
|
|
33
|
+
"lua",
|
|
34
|
+
"r",
|
|
35
|
+
"julia",
|
|
36
|
+
"powershell",
|
|
37
|
+
"haskell",
|
|
38
|
+
"elixir",
|
|
39
|
+
"erlang",
|
|
40
|
+
"clojure",
|
|
41
|
+
"ocaml",
|
|
42
|
+
"fsharp",
|
|
43
|
+
"json",
|
|
44
|
+
"yaml",
|
|
45
|
+
"toml",
|
|
46
|
+
"ini",
|
|
47
|
+
"xml",
|
|
48
|
+
"html",
|
|
49
|
+
"css",
|
|
50
|
+
"graphql",
|
|
51
|
+
"proto",
|
|
52
|
+
"dockerfile",
|
|
53
|
+
"makefile",
|
|
54
|
+
"asm",
|
|
55
|
+
"elm",
|
|
56
|
+
"nim",
|
|
57
|
+
"crystal",
|
|
58
|
+
"solidity",
|
|
59
|
+
"hcl",
|
|
60
|
+
"nix",
|
|
61
|
+
"matlab",
|
|
62
|
+
"fortran",
|
|
63
|
+
"pascal",
|
|
64
|
+
"groovy",
|
|
65
|
+
"verilog",
|
|
66
|
+
"vhdl",
|
|
67
|
+
"latex",
|
|
68
|
+
"objc",
|
|
69
|
+
"scheme",
|
|
70
|
+
"lisp",
|
|
71
|
+
"prolog",
|
|
72
|
+
"ada",
|
|
73
|
+
"d",
|
|
74
|
+
"gleam",
|
|
75
|
+
"batch",
|
|
76
|
+
"awk",
|
|
77
|
+
"tcl",
|
|
78
|
+
"vb",
|
|
79
|
+
"scss",
|
|
80
|
+
"nginx",
|
|
81
|
+
"jinja",
|
|
82
|
+
"sparql",
|
|
83
|
+
"cypher",
|
|
84
|
+
"fish",
|
|
85
|
+
"apex",
|
|
86
|
+
"purescript",
|
|
87
|
+
"odin",
|
|
88
|
+
"zsh",
|
|
89
|
+
"diff",
|
|
90
|
+
"md",
|
|
91
|
+
] as const;
|
|
92
|
+
export interface CodeLanguageRequest {
|
|
93
|
+
blockId: string;
|
|
94
|
+
expected: string | null;
|
|
95
|
+
language: string | null;
|
|
96
|
+
}
|
|
97
|
+
export interface CodeLanguagePreview {
|
|
98
|
+
version: 1;
|
|
99
|
+
kind: "displayed-code-language";
|
|
100
|
+
sourceHash: string;
|
|
101
|
+
requests: CodeLanguageRequest[];
|
|
102
|
+
patches: SourcePatch[];
|
|
103
|
+
result: string;
|
|
104
|
+
resultHash: string;
|
|
105
|
+
}
|
|
106
|
+
function fail(message: string): never {
|
|
107
|
+
throw new SourceError("unsupported-code-language", message);
|
|
108
|
+
}
|
|
109
|
+
export function checkedCodeLanguage(value: unknown): string | null {
|
|
110
|
+
if (value === null) return null;
|
|
111
|
+
if (
|
|
112
|
+
typeof value !== "string" ||
|
|
113
|
+
!DISPLAYED_CODE_LANGUAGES.includes(
|
|
114
|
+
value as typeof DISPLAYED_CODE_LANGUAGES[number],
|
|
115
|
+
)
|
|
116
|
+
) {
|
|
117
|
+
fail(
|
|
118
|
+
"Select an exact built-in Bento language ID, or remove the language; executable syntax and unknown aliases are unsupported.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
export function codeLanguageSyntax(raw: string) {
|
|
124
|
+
const line = raw.split(/\r?\n/)[0],
|
|
125
|
+
fence = /^ {0,3}(`{3,}|~{3,})([ \t]*)(.*?)([ \t]*)$/.exec(line);
|
|
126
|
+
if (!fence) {
|
|
127
|
+
fail("Displayed code requires one exact original non-executable fence.");
|
|
128
|
+
}
|
|
129
|
+
const prefix = line.indexOf(fence[1]) + fence[1].length + fence[2].length,
|
|
130
|
+
info = fence[3];
|
|
131
|
+
if (info.startsWith("{")) {
|
|
132
|
+
if (!/^\{[^{}]*\}$/.test(info)) {
|
|
133
|
+
fail("Executable/raw/nested fence syntax cannot change language.");
|
|
134
|
+
}
|
|
135
|
+
const content = info.slice(1, -1),
|
|
136
|
+
tokens = originalAttributeTokens(content),
|
|
137
|
+
attr = originalPandocAttributes(content, tokens);
|
|
138
|
+
if (attr[1].length > 1) {
|
|
139
|
+
fail("Displayed code must have at most one original language class.");
|
|
140
|
+
}
|
|
141
|
+
const token = tokens.find((t) => !t.key && content[t.start] === ".");
|
|
142
|
+
return {
|
|
143
|
+
grammar: attr[1][0] ?? null,
|
|
144
|
+
start: prefix + 1 + (token?.start ?? content.length),
|
|
145
|
+
end: prefix + 1 + (token?.end ?? content.length),
|
|
146
|
+
attribute: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (/[\s{}]/.test(info)) {
|
|
150
|
+
fail("Plain displayed fences require one literal language token.");
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
grammar: info || null,
|
|
154
|
+
start: prefix,
|
|
155
|
+
end: prefix + info.length,
|
|
156
|
+
attribute: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const encoder = new TextEncoder(),
|
|
160
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
161
|
+
function splice(text: string, patches: SourcePatch[]) {
|
|
162
|
+
let bytes = encoder.encode(text);
|
|
163
|
+
for (const p of [...patches].sort((a, b) => b.span.start - a.span.start)) {
|
|
164
|
+
if (
|
|
165
|
+
decoder.decode(bytes.subarray(p.span.start, p.span.end)) !== p.expected
|
|
166
|
+
) throw new SourceError("stale-fragment", p.blockId);
|
|
167
|
+
const replacement = encoder.encode(p.replacement),
|
|
168
|
+
next = new Uint8Array(
|
|
169
|
+
bytes.length - p.span.end + p.span.start + replacement.length,
|
|
170
|
+
);
|
|
171
|
+
next.set(bytes.subarray(0, p.span.start));
|
|
172
|
+
next.set(replacement, p.span.start);
|
|
173
|
+
next.set(bytes.subarray(p.span.end), p.span.start + replacement.length);
|
|
174
|
+
bytes = next;
|
|
175
|
+
}
|
|
176
|
+
return decoder.decode(bytes);
|
|
177
|
+
}
|
|
178
|
+
export function previewCodeLanguages(
|
|
179
|
+
doc: SourceDocument,
|
|
180
|
+
requests: CodeLanguageRequest[],
|
|
181
|
+
): CodeLanguagePreview {
|
|
182
|
+
if (hashSource(doc.text) !== doc.hash) {
|
|
183
|
+
throw new SourceError("stale-document", "Source snapshot changed.");
|
|
184
|
+
}
|
|
185
|
+
const seen = new Set<string>(), patches: SourcePatch[] = [];
|
|
186
|
+
for (const r of requests) {
|
|
187
|
+
if (
|
|
188
|
+
!r || Object.keys(r).sort().join(",") !== "blockId,expected,language" ||
|
|
189
|
+
typeof r.blockId !== "string" ||
|
|
190
|
+
!(r.expected === null || typeof r.expected === "string") ||
|
|
191
|
+
seen.has(r.blockId)
|
|
192
|
+
) fail("Malformed or duplicate code language request.");
|
|
193
|
+
seen.add(r.blockId);
|
|
194
|
+
checkedCodeLanguage(r.language);
|
|
195
|
+
const blocks = doc.blocks.filter((b) => b.id === r.blockId), b = blocks[0];
|
|
196
|
+
if (
|
|
197
|
+
blocks.length !== 1 || b.kind !== "code" || b.origin !== "authored" ||
|
|
198
|
+
!b.editable
|
|
199
|
+
) fail("Language sync is limited to one authored non-executable fence.");
|
|
200
|
+
const syntax = codeLanguageSyntax(b.raw);
|
|
201
|
+
if (syntax.grammar !== r.expected) {
|
|
202
|
+
throw new SourceError(
|
|
203
|
+
"source-conflict",
|
|
204
|
+
"Original displayed language changed; recalculate before reviewing.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const line = b.raw.split(/\r?\n/)[0],
|
|
208
|
+
expected = line.slice(syntax.start, syntax.end),
|
|
209
|
+
replacement = r.language === null
|
|
210
|
+
? ""
|
|
211
|
+
: syntax.attribute
|
|
212
|
+
? (syntax.grammar ? "." + r.language : " ." + r.language)
|
|
213
|
+
: r.language;
|
|
214
|
+
const start = b.span.start +
|
|
215
|
+
encoder.encode(line.slice(0, syntax.start)).length,
|
|
216
|
+
end = b.span.start + encoder.encode(line.slice(0, syntax.end)).length;
|
|
217
|
+
patches.push({
|
|
218
|
+
blockId: b.id,
|
|
219
|
+
span: { start, end },
|
|
220
|
+
expected,
|
|
221
|
+
replacement,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const result = splice(doc.text, patches), fresh = scanSource(result);
|
|
225
|
+
const shape = (d: SourceDocument) =>
|
|
226
|
+
d.blocks.map(
|
|
227
|
+
(b) => [
|
|
228
|
+
b.kind,
|
|
229
|
+
b.explicitId,
|
|
230
|
+
b.level,
|
|
231
|
+
b.origin,
|
|
232
|
+
b.kind === "div" ? null : b.content,
|
|
233
|
+
],
|
|
234
|
+
);
|
|
235
|
+
if (
|
|
236
|
+
JSON.stringify(shape(doc)) !== JSON.stringify(shape(fresh)) ||
|
|
237
|
+
fresh.diagnostics.length
|
|
238
|
+
) {
|
|
239
|
+
fail(
|
|
240
|
+
"Language edit changed source body, identity or execution boundaries.",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
for (const r of requests) {
|
|
244
|
+
const b = fresh.blocks.find((b) => b.id === r.blockId);
|
|
245
|
+
if (!b || codeLanguageSyntax(b.raw).grammar !== r.language) {
|
|
246
|
+
fail("Language does not round-trip exactly.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
version: 1,
|
|
251
|
+
kind: "displayed-code-language",
|
|
252
|
+
sourceHash: doc.hash,
|
|
253
|
+
requests: structuredClone(requests),
|
|
254
|
+
patches,
|
|
255
|
+
result,
|
|
256
|
+
resultHash: hashSource(result),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function applyCodeLanguages(text: string, preview: CodeLanguagePreview) {
|
|
260
|
+
if (
|
|
261
|
+
preview.version !== 1 || preview.kind !== "displayed-code-language" ||
|
|
262
|
+
hashSource(text) !== preview.sourceHash
|
|
263
|
+
) {
|
|
264
|
+
throw new SourceError(
|
|
265
|
+
"stale-source",
|
|
266
|
+
"Review the current code language patch.",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const verified = previewCodeLanguages(scanSource(text), preview.requests);
|
|
270
|
+
if (JSON.stringify(verified) !== JSON.stringify(preview)) {
|
|
271
|
+
throw new SourceError(
|
|
272
|
+
"invalid-preview",
|
|
273
|
+
"Code language preview changed after review.",
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return verified.result;
|
|
277
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** Bounded component source; no design geometry, source execution or inferred roles. */
|
|
2
|
+
import { scanSource, type SourceDocument, SourceError } from "./mod.ts";
|
|
3
|
+
import { kpiSource } from "./kpi.ts";
|
|
4
|
+
export interface ComponentInsertion {
|
|
5
|
+
type: "insert-component";
|
|
6
|
+
kind: "card" | "kpi";
|
|
7
|
+
slideId: string;
|
|
8
|
+
id: string;
|
|
9
|
+
fields: {
|
|
10
|
+
role: "title" | "body" | "label" | "value" | "detail";
|
|
11
|
+
sourceId: string;
|
|
12
|
+
markdown: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
function fail(message: string): never {
|
|
16
|
+
throw new SourceError("invalid-component-insertion", message);
|
|
17
|
+
}
|
|
18
|
+
export function componentMarkdown(operation: ComponentInsertion): string {
|
|
19
|
+
const keys = (value: object, expected: string[]) =>
|
|
20
|
+
Object.keys(value).length === expected.length &&
|
|
21
|
+
expected.every((key) => Object.hasOwn(value, key));
|
|
22
|
+
if (
|
|
23
|
+
!operation ||
|
|
24
|
+
!keys(operation, ["type", "kind", "slideId", "id", "fields"]) ||
|
|
25
|
+
operation.type !== "insert-component" ||
|
|
26
|
+
!["card", "kpi"].includes(operation.kind) ||
|
|
27
|
+
!/^[A-Za-z][\w:.-]{0,119}$/.test(operation.id) ||
|
|
28
|
+
typeof operation.slideId !== "string" || !Array.isArray(operation.fields)
|
|
29
|
+
) fail("Use one explicit identified card or KPI with exact authored roles.");
|
|
30
|
+
const roles = operation.kind === "card"
|
|
31
|
+
? ["title", "body"]
|
|
32
|
+
: ["label", "value", ...(operation.fields.length === 3 ? ["detail"] : [])];
|
|
33
|
+
if (operation.fields.length !== roles.length) {
|
|
34
|
+
fail("Use title/body or label/value/optional detail in that order.");
|
|
35
|
+
}
|
|
36
|
+
const pieces = [`:::: {#${operation.id} .bento-${operation.kind}}`];
|
|
37
|
+
for (const [index, field] of operation.fields.entries()) {
|
|
38
|
+
if (
|
|
39
|
+
!field || !keys(field, ["role", "sourceId", "markdown"]) ||
|
|
40
|
+
field.role !== roles[index] ||
|
|
41
|
+
field.sourceId !== `${operation.id}-${field.role}` ||
|
|
42
|
+
typeof field.markdown !== "string" || !field.markdown.trim() ||
|
|
43
|
+
field.markdown.length > 12000
|
|
44
|
+
) {
|
|
45
|
+
fail(
|
|
46
|
+
"Each authored field needs its explicit role and reserved source identity.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const markdown = field.markdown.replace(/\r\n|\r/g, "\n"),
|
|
50
|
+
parsed = scanSource(markdown),
|
|
51
|
+
block = parsed.blocks[0];
|
|
52
|
+
if (
|
|
53
|
+
parsed.diagnostics.length || parsed.blocks.length !== 1 ||
|
|
54
|
+
block.kind !== "paragraph" || !block.editable ||
|
|
55
|
+
block.origin !== "authored" || block.explicitId || block.parentId ||
|
|
56
|
+
block.content !== markdown || /<!--|-->|\{\{[<%]|\$\$|\\\[/.test(markdown)
|
|
57
|
+
) {
|
|
58
|
+
fail(
|
|
59
|
+
"Fields must be one complete authored paragraph, with no structural, computed or display-math syntax.",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (field.role === "title") {
|
|
63
|
+
if (/[\r\n]/.test(markdown)) fail("A card title must be a single line.");
|
|
64
|
+
pieces.push(`### ${markdown} {#${field.sourceId}}`);
|
|
65
|
+
} else {
|
|
66
|
+
pieces.push(
|
|
67
|
+
`::: {#${field.sourceId}${
|
|
68
|
+
operation.kind === "kpi" ? ` bento-role="${field.role}"` : ""
|
|
69
|
+
}}\n${markdown}\n:::`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
pieces.push("::::");
|
|
74
|
+
const result = pieces.join("\n\n") + "\n\n", parsed = scanSource(result);
|
|
75
|
+
if (
|
|
76
|
+
parsed.diagnostics.length ||
|
|
77
|
+
parsed.blocks.some((block) => block.origin !== "authored")
|
|
78
|
+
) fail("Component fields introduced unsupported syntax.");
|
|
79
|
+
if (operation.kind === "kpi") kpiSource(parsed, operation.id);
|
|
80
|
+
else {
|
|
81
|
+
const direct = parsed.blocks.filter((block) =>
|
|
82
|
+
block.parentId === operation.id
|
|
83
|
+
);
|
|
84
|
+
if (
|
|
85
|
+
direct.length !== 2 || direct[0].kind !== "heading" ||
|
|
86
|
+
direct[0].level !== 3 || direct[0].id !== `${operation.id}-title` ||
|
|
87
|
+
direct[0].content !== operation.fields[0].markdown ||
|
|
88
|
+
direct[1].kind !== "div" || direct[1].id !== `${operation.id}-body`
|
|
89
|
+
) fail("Card title/body must preserve exact source roles.");
|
|
90
|
+
const body = parsed.blocks.filter((block) =>
|
|
91
|
+
block.parentId === direct[1].id
|
|
92
|
+
);
|
|
93
|
+
if (
|
|
94
|
+
body.length !== 1 || body[0].kind !== "paragraph" ||
|
|
95
|
+
body[0].id !== `${direct[1].id}/paragraph:1`
|
|
96
|
+
) fail("Card body must preserve one source paragraph.");
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
export function generatedComponent(
|
|
101
|
+
source: SourceDocument,
|
|
102
|
+
operation: ComponentInsertion,
|
|
103
|
+
): string {
|
|
104
|
+
const section = componentMarkdown(operation),
|
|
105
|
+
existing = new Set(source.blocks.map((block) => block.id));
|
|
106
|
+
for (const block of scanSource(section).blocks) {
|
|
107
|
+
if (existing.has(block.id)) {
|
|
108
|
+
throw new SourceError(
|
|
109
|
+
"duplicate-id",
|
|
110
|
+
`Source identity ${block.id} already exists.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return section.replace(/\n/g, source.newline);
|
|
115
|
+
}
|
|
116
|
+
export function verifyComponentInsertion(
|
|
117
|
+
before: SourceDocument,
|
|
118
|
+
after: SourceDocument,
|
|
119
|
+
operation: ComponentInsertion,
|
|
120
|
+
): void {
|
|
121
|
+
const expected = scanSource(componentMarkdown(operation)),
|
|
122
|
+
ids = new Set(expected.blocks.map((block) => block.id));
|
|
123
|
+
const added = after.blocks.filter((block) => ids.has(block.id));
|
|
124
|
+
const semantic = (blocks: SourceDocument["blocks"]) =>
|
|
125
|
+
blocks.map((
|
|
126
|
+
block,
|
|
127
|
+
) => [
|
|
128
|
+
block.id,
|
|
129
|
+
block.kind,
|
|
130
|
+
block.origin,
|
|
131
|
+
block.parentId,
|
|
132
|
+
block.content.replace(/\r\n/g, "\n"),
|
|
133
|
+
block.level,
|
|
134
|
+
]);
|
|
135
|
+
if (
|
|
136
|
+
JSON.stringify(semantic(added)) !==
|
|
137
|
+
JSON.stringify(semantic(expected.blocks)) ||
|
|
138
|
+
added.some((block) => block.slideId !== operation.slideId) ||
|
|
139
|
+
JSON.stringify(before.slides.map((slide) => [slide.id, slide.stable])) !==
|
|
140
|
+
JSON.stringify(after.slides.map((slide) => [slide.id, slide.stable]))
|
|
141
|
+
) {
|
|
142
|
+
fail(
|
|
143
|
+
"Insertion must preserve exact component roles and existing slide boundaries.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const prior = (blocks: SourceDocument["blocks"]) =>
|
|
147
|
+
blocks.map((
|
|
148
|
+
block,
|
|
149
|
+
) => [
|
|
150
|
+
block.kind,
|
|
151
|
+
block.raw.replace(/\r?\n$/, ""),
|
|
152
|
+
block.origin,
|
|
153
|
+
block.level,
|
|
154
|
+
block.slideId,
|
|
155
|
+
]);
|
|
156
|
+
if (
|
|
157
|
+
JSON.stringify(prior(before.blocks)) !==
|
|
158
|
+
JSON.stringify(prior(after.blocks.filter((block) => !ids.has(block.id))))
|
|
159
|
+
) fail("Insertion changed existing source bytes or block boundaries.");
|
|
160
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/** Exact removal of one independently authored source unit, without container inference. */
|
|
2
|
+
import {
|
|
3
|
+
sliceBytes,
|
|
4
|
+
type SourceBlock,
|
|
5
|
+
type SourceDocument,
|
|
6
|
+
SourceError,
|
|
7
|
+
type Span,
|
|
8
|
+
} from "./mod.ts";
|
|
9
|
+
import {
|
|
10
|
+
originalAttributeTokens,
|
|
11
|
+
originalPandocAttributes,
|
|
12
|
+
} from "./attributes.ts";
|
|
13
|
+
import { isSourceIdentity } from "./identity-id.ts";
|
|
14
|
+
import { nativeDesignField, parseDesignAttributes } from "../render/design.ts";
|
|
15
|
+
|
|
16
|
+
function fail(code: string, message: string): never {
|
|
17
|
+
throw new SourceError(code, message);
|
|
18
|
+
}
|
|
19
|
+
function escapeRegex(text: string): string {
|
|
20
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
}
|
|
22
|
+
function hasHtmlReference(text: string, id: string): boolean {
|
|
23
|
+
for (const tag of text.matchAll(/<[a-z](?:[^"'<>]|"[^"]*"|'[^']*')*>/gi)) {
|
|
24
|
+
for (
|
|
25
|
+
const attr of tag[0].matchAll(
|
|
26
|
+
/\s([^\s"'<>/=]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g,
|
|
27
|
+
)
|
|
28
|
+
) {
|
|
29
|
+
if (!["href", "xlink:href"].includes(attr[1].toLowerCase())) continue;
|
|
30
|
+
const value = (attr[2] ?? attr[3] ?? attr[4]).replace(
|
|
31
|
+
/&#(?:x([\da-f]+)|(\d+));?|&(num|colon|period|lowbar|hyphen);/gi,
|
|
32
|
+
(_, hex, decimal, named) => {
|
|
33
|
+
if (named) {
|
|
34
|
+
return ({
|
|
35
|
+
num: "#",
|
|
36
|
+
colon: ":",
|
|
37
|
+
period: ".",
|
|
38
|
+
lowbar: "_",
|
|
39
|
+
hyphen: "-",
|
|
40
|
+
} as Record<string, string>)[named.toLowerCase()];
|
|
41
|
+
}
|
|
42
|
+
const code = parseInt(hex ?? decimal, hex ? 16 : 10);
|
|
43
|
+
return code <= 0x10ffff ? String.fromCodePoint(code) : "\ufffd";
|
|
44
|
+
},
|
|
45
|
+
).trim();
|
|
46
|
+
if (anchorMatches(value, id)) return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function anchorMatches(value: string, id: string): boolean {
|
|
52
|
+
if (!value.startsWith("#")) return false;
|
|
53
|
+
try {
|
|
54
|
+
return decodeURIComponent(value.slice(1)) === id;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Shared dependency guard for verified slide or element deletion. */
|
|
60
|
+
export function assertDeletionReferences(rest: string, ids: string[]): void {
|
|
61
|
+
for (const original of ids) {
|
|
62
|
+
const id = escapeRegex(original);
|
|
63
|
+
const markdownLinks = rest.matchAll(
|
|
64
|
+
/(?:\]\(\s*(?:<([^>]+)>|([^\s)]+))|^\s*\[[^\]]+\]:\s*(?:<([^>]+)>|(\S+)))/gm,
|
|
65
|
+
);
|
|
66
|
+
if (
|
|
67
|
+
new RegExp(`@${id}(?:\\.(?![\\w:.-])|(?![\\w:.-]))`).test(rest) ||
|
|
68
|
+
[...markdownLinks].some((match) =>
|
|
69
|
+
anchorMatches(match[1] ?? match[2] ?? match[3] ?? match[4], original)
|
|
70
|
+
) || hasHtmlReference(rest, original)
|
|
71
|
+
) {
|
|
72
|
+
fail(
|
|
73
|
+
"shared-reference",
|
|
74
|
+
`Other source references ${original}; deletion requires dependency resolution.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Include only this unit's immediately preceding technical identity marker. */
|
|
80
|
+
function withMarker(doc: SourceDocument, block: SourceBlock): Span {
|
|
81
|
+
const before = sliceBytes(doc.text, { start: 0, end: block.span.start });
|
|
82
|
+
const marker = before.match(
|
|
83
|
+
/(?:^|\n)([ \t]*<!--[ \t]*qb:([^\s<>]+)[ \t]*-->[ \t]*(?:(?:\r\n|\n)[ \t]*)+)$/,
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
start: marker?.[2] === block.id && isSourceIdentity(marker[2])
|
|
87
|
+
? block.span.start - new TextEncoder().encode(marker[1]).length
|
|
88
|
+
: block.span.start,
|
|
89
|
+
end: block.span.end,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export interface SourceDeletion {
|
|
93
|
+
blockId: string;
|
|
94
|
+
removedIds: string[];
|
|
95
|
+
span: Span;
|
|
96
|
+
expected: string;
|
|
97
|
+
replacement: "";
|
|
98
|
+
}
|
|
99
|
+
/** A plain wrapper may provide identity/design, but cannot stand for a semantic component. */
|
|
100
|
+
export function pureWrapper(
|
|
101
|
+
doc: SourceDocument,
|
|
102
|
+
parent: SourceBlock,
|
|
103
|
+
block: SourceBlock,
|
|
104
|
+
): void {
|
|
105
|
+
const opener = parent.raw.split(/\r?\n/)[0].match(
|
|
106
|
+
/^ {0,3}(:{3,})[ \t]+\{([^{}]+)\}[ \t]*$/,
|
|
107
|
+
);
|
|
108
|
+
if (!opener) {
|
|
109
|
+
fail(
|
|
110
|
+
"unsafe-element-delete",
|
|
111
|
+
"The containing div has no verified standalone boundary.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const attr = originalPandocAttributes(
|
|
115
|
+
opener[2],
|
|
116
|
+
originalAttributeTokens(opener[2]),
|
|
117
|
+
);
|
|
118
|
+
parseDesignAttributes(attr);
|
|
119
|
+
if (
|
|
120
|
+
attr[0] !== parent.id || attr[1].length ||
|
|
121
|
+
attr[2].some(([key]) =>
|
|
122
|
+
key !== "bento-element-id" && !nativeDesignField(key)
|
|
123
|
+
) ||
|
|
124
|
+
new Set(attr[2].map(([key]) => key)).size !== attr[2].length
|
|
125
|
+
) {
|
|
126
|
+
fail(
|
|
127
|
+
"unsafe-element-delete",
|
|
128
|
+
"A semantic component or unverified container must be edited explicitly in QMD.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const descendants = doc.blocks.filter((candidate) =>
|
|
132
|
+
candidate !== parent && candidate.span.start >= parent.span.start &&
|
|
133
|
+
candidate.span.end <= parent.span.end
|
|
134
|
+
);
|
|
135
|
+
if (descendants.length !== 1 || descendants[0] !== block) {
|
|
136
|
+
fail(
|
|
137
|
+
"unsafe-element-delete",
|
|
138
|
+
"The containing div must contain exactly one independent authored element.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const leading = sliceBytes(doc.text, {
|
|
142
|
+
start: parent.contentSpan.start,
|
|
143
|
+
end: block.span.start,
|
|
144
|
+
});
|
|
145
|
+
const trailing = sliceBytes(doc.text, {
|
|
146
|
+
start: block.span.end,
|
|
147
|
+
end: parent.contentSpan.end,
|
|
148
|
+
});
|
|
149
|
+
// Scanner ignores comments; refuse hidden syntax except the child's own identity marker.
|
|
150
|
+
const cleanLeading = block.explicitId
|
|
151
|
+
? leading.replace(
|
|
152
|
+
new RegExp(
|
|
153
|
+
`^[ \t]*<!--[ \t]*qb:${
|
|
154
|
+
escapeRegex(block.id)
|
|
155
|
+
}[ \t]*-->[ \t]*(?:\r?\n|$)`,
|
|
156
|
+
"m",
|
|
157
|
+
),
|
|
158
|
+
"",
|
|
159
|
+
)
|
|
160
|
+
: leading;
|
|
161
|
+
const closer = sliceBytes(doc.text, {
|
|
162
|
+
start: parent.contentSpan.end,
|
|
163
|
+
end: parent.span.end,
|
|
164
|
+
});
|
|
165
|
+
if (cleanLeading.trim() || trailing.trim() || closer.trim() !== opener[1]) {
|
|
166
|
+
fail(
|
|
167
|
+
"unsafe-element-delete",
|
|
168
|
+
"The containing div includes unverified syntax or a mismatched closing fence.",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Validate the source-only portion; core also verifies its exact executed provenance. */
|
|
173
|
+
export function sourceDeletion(
|
|
174
|
+
doc: SourceDocument,
|
|
175
|
+
blockId: string,
|
|
176
|
+
): SourceDeletion {
|
|
177
|
+
if (doc.diagnostics.length) {
|
|
178
|
+
fail(
|
|
179
|
+
"ambiguous-structure",
|
|
180
|
+
"Resolve source diagnostics before deleting an element.",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const matches = doc.blocks.filter((candidate) => candidate.id === blockId);
|
|
184
|
+
const block = matches[0];
|
|
185
|
+
if (
|
|
186
|
+
matches.length !== 1 || !block.editable || block.origin !== "authored" ||
|
|
187
|
+
!["paragraph", "list", "code", "math"].includes(block.kind) ||
|
|
188
|
+
!block.slideId
|
|
189
|
+
) {
|
|
190
|
+
fail(
|
|
191
|
+
"unsafe-element-delete",
|
|
192
|
+
"Only one verified authored paragraph, list, displayed code or display equation can be deleted; headings and computed producers require explicit QMD authoring.",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
let unit = block;
|
|
196
|
+
if (block.parentId) {
|
|
197
|
+
const parents = doc.blocks.filter((candidate) =>
|
|
198
|
+
candidate.id === block.parentId
|
|
199
|
+
);
|
|
200
|
+
const parent = parents[0];
|
|
201
|
+
if (
|
|
202
|
+
parents.length !== 1 || parent.kind !== "div" || !parent.stable ||
|
|
203
|
+
parent.parentId
|
|
204
|
+
) {
|
|
205
|
+
fail(
|
|
206
|
+
"unsafe-element-delete",
|
|
207
|
+
"The element is nested inside an ambiguous or shared source container.",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
pureWrapper(doc, parent, block);
|
|
211
|
+
unit = parent;
|
|
212
|
+
} else if (!block.stable) {
|
|
213
|
+
fail(
|
|
214
|
+
"unsafe-element-delete",
|
|
215
|
+
"Pin this standalone source element and recalculate before deleting it.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const span = withMarker(doc, unit);
|
|
219
|
+
const removed = doc.blocks.filter((candidate) =>
|
|
220
|
+
candidate.span.start >= unit.span.start &&
|
|
221
|
+
candidate.span.end <= unit.span.end
|
|
222
|
+
);
|
|
223
|
+
if (removed.some((candidate) => candidate.origin !== "authored")) {
|
|
224
|
+
fail(
|
|
225
|
+
"unsafe-element-delete",
|
|
226
|
+
"Deletion would remove a computed or shared producer.",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const rest = sliceBytes(doc.text, { start: 0, end: span.start }) +
|
|
230
|
+
sliceBytes(doc.text, { start: span.end, end: doc.byteLength });
|
|
231
|
+
assertDeletionReferences(
|
|
232
|
+
rest,
|
|
233
|
+
removed.flatMap((candidate) =>
|
|
234
|
+
candidate.explicitId ? [candidate.explicitId] : []
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
return {
|
|
238
|
+
blockId,
|
|
239
|
+
removedIds: removed.map((candidate) => candidate.id),
|
|
240
|
+
span,
|
|
241
|
+
expected: sliceBytes(doc.text, span),
|
|
242
|
+
replacement: "",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/** Removing a unit must leave all other semantic boundaries, identities and bytes intact. */
|
|
246
|
+
export function verifySourceDeletion(
|
|
247
|
+
before: SourceDocument,
|
|
248
|
+
after: SourceDocument,
|
|
249
|
+
blockId: string,
|
|
250
|
+
): void {
|
|
251
|
+
const removed = new Set(sourceDeletion(before, blockId).removedIds);
|
|
252
|
+
const shape = (blocks: SourceBlock[]) =>
|
|
253
|
+
blocks.map((block) => [
|
|
254
|
+
block.id,
|
|
255
|
+
block.explicitId,
|
|
256
|
+
block.kind,
|
|
257
|
+
block.raw,
|
|
258
|
+
block.content,
|
|
259
|
+
block.origin,
|
|
260
|
+
block.editable,
|
|
261
|
+
block.parentId,
|
|
262
|
+
block.slideId,
|
|
263
|
+
block.level,
|
|
264
|
+
]);
|
|
265
|
+
if (
|
|
266
|
+
after.diagnostics.length ||
|
|
267
|
+
JSON.stringify(
|
|
268
|
+
shape(before.blocks.filter((block) => !removed.has(block.id))),
|
|
269
|
+
) !== JSON.stringify(shape(after.blocks)) ||
|
|
270
|
+
JSON.stringify(before.slides.map((slide) => [slide.id, slide.stable])) !==
|
|
271
|
+
JSON.stringify(after.slides.map((slide) => [slide.id, slide.stable]))
|
|
272
|
+
) {
|
|
273
|
+
fail(
|
|
274
|
+
"unsafe-element-delete",
|
|
275
|
+
"Deletion would change another source boundary or identity. Pin the remaining blocks or repair the surrounding QMD and recalculate before retrying.",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|