@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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -2
  3. package/bin/qbento-npm.mjs +62 -0
  4. package/distribution/LICENSE +21 -0
  5. package/distribution/PACKAGING.md +59 -0
  6. package/distribution/bin/qbento +5 -0
  7. package/distribution/bin/qbento.cmd +4 -0
  8. package/distribution/manifest.json +720 -0
  9. package/distribution/runtime/LICENSE +21 -0
  10. package/distribution/runtime/_extensions/qbento/_extension.yml +24 -0
  11. package/distribution/runtime/_extensions/qbento/capture.lua +21 -0
  12. package/distribution/runtime/_extensions/qbento/guard.lua +7 -0
  13. package/distribution/runtime/_extensions/qbento/post.ts +2 -0
  14. package/distribution/runtime/_extensions/qbento/pre.ts +2 -0
  15. package/distribution/runtime/_extensions/qbento/producer-activation.js +129 -0
  16. package/distribution/runtime/src/bridge/browser.js +168 -0
  17. package/distribution/runtime/src/bridge/isolation.ts +55 -0
  18. package/distribution/runtime/src/browser/chromium.ts +218 -0
  19. package/distribution/runtime/src/cli.ts +232 -0
  20. package/distribution/runtime/src/core/agent.ts +546 -0
  21. package/distribution/runtime/src/core/annotations.ts +384 -0
  22. package/distribution/runtime/src/core/capture.ts +203 -0
  23. package/distribution/runtime/src/core/component-actions.ts +550 -0
  24. package/distribution/runtime/src/core/component-frames.ts +59 -0
  25. package/distribution/runtime/src/core/component-sync.ts +290 -0
  26. package/distribution/runtime/src/core/design-diagnostics.ts +78 -0
  27. package/distribution/runtime/src/core/design-registry.ts +287 -0
  28. package/distribution/runtime/src/core/graph-relations.ts +354 -0
  29. package/distribution/runtime/src/core/graph-scope.ts +63 -0
  30. package/distribution/runtime/src/core/graph-sync.ts +228 -0
  31. package/distribution/runtime/src/core/io.ts +27 -0
  32. package/distribution/runtime/src/core/kpi-appearance.ts +205 -0
  33. package/distribution/runtime/src/core/layouts.ts +245 -0
  34. package/distribution/runtime/src/core/project.ts +851 -0
  35. package/distribution/runtime/src/core/reconcile.ts +384 -0
  36. package/distribution/runtime/src/core/recovery.ts +1255 -0
  37. package/distribution/runtime/src/core/slide-actions.ts +158 -0
  38. package/distribution/runtime/src/core/source-scope.ts +50 -0
  39. package/distribution/runtime/src/core/sync.ts +2557 -0
  40. package/distribution/runtime/src/core/widgets.ts +300 -0
  41. package/distribution/runtime/src/integration/install.ts +98 -0
  42. package/distribution/runtime/src/integration/invocation.ts +100 -0
  43. package/distribution/runtime/src/integration/quarto.ts +457 -0
  44. package/distribution/runtime/src/render/assets.ts +66 -0
  45. package/distribution/runtime/src/render/capture.lua +8 -0
  46. package/distribution/runtime/src/render/chart.ts +165 -0
  47. package/distribution/runtime/src/render/components.ts +199 -0
  48. package/distribution/runtime/src/render/design.ts +820 -0
  49. package/distribution/runtime/src/render/diagnostics.ts +36 -0
  50. package/distribution/runtime/src/render/graphs.ts +747 -0
  51. package/distribution/runtime/src/render/html.ts +82 -0
  52. package/distribution/runtime/src/render/images.ts +268 -0
  53. package/distribution/runtime/src/render/inline.ts +259 -0
  54. package/distribution/runtime/src/render/kpi.ts +385 -0
  55. package/distribution/runtime/src/render/lists.ts +183 -0
  56. package/distribution/runtime/src/render/math.ts +413 -0
  57. package/distribution/runtime/src/render/measure.ts +185 -0
  58. package/distribution/runtime/src/render/native-layout.ts +174 -0
  59. package/distribution/runtime/src/render/producer-capture.ts +319 -0
  60. package/distribution/runtime/src/render/producer-environment.R +49 -0
  61. package/distribution/runtime/src/render/producer-environment.ts +96 -0
  62. package/distribution/runtime/src/render/producer-profile.R +91 -0
  63. package/distribution/runtime/src/render/producer-site-profile.R +18 -0
  64. package/distribution/runtime/src/render/project.ts +1806 -0
  65. package/distribution/runtime/src/render/quarto.ts +127 -0
  66. package/distribution/runtime/src/render/registry.ts +233 -0
  67. package/distribution/runtime/src/render/resources.ts +292 -0
  68. package/distribution/runtime/src/render/shell.ts +110 -0
  69. package/distribution/runtime/src/render/source-inline.ts +169 -0
  70. package/distribution/runtime/src/render/table.ts +220 -0
  71. package/distribution/runtime/src/render/text-overflow.ts +208 -0
  72. package/distribution/runtime/src/render/types.ts +71 -0
  73. package/distribution/runtime/src/render/validate.ts +572 -0
  74. package/distribution/runtime/src/rich/project.ts +117 -0
  75. package/distribution/runtime/src/rich/table.ts +374 -0
  76. package/distribution/runtime/src/source/annotations.ts +490 -0
  77. package/distribution/runtime/src/source/attributes.ts +415 -0
  78. package/distribution/runtime/src/source/chart.ts +165 -0
  79. package/distribution/runtime/src/source/code-language.ts +277 -0
  80. package/distribution/runtime/src/source/component-insertion.ts +160 -0
  81. package/distribution/runtime/src/source/deletion.ts +278 -0
  82. package/distribution/runtime/src/source/graphs.ts +517 -0
  83. package/distribution/runtime/src/source/identities.ts +256 -0
  84. package/distribution/runtime/src/source/identity-id.ts +6 -0
  85. package/distribution/runtime/src/source/image.ts +50 -0
  86. package/distribution/runtime/src/source/kpi.ts +196 -0
  87. package/distribution/runtime/src/source/mod.ts +876 -0
  88. package/distribution/runtime/src/source/notes.ts +308 -0
  89. package/distribution/runtime/src/source/structure.ts +647 -0
  90. package/distribution/runtime/src/source/table.ts +290 -0
  91. package/distribution/runtime/src/source/text-conversion.ts +146 -0
  92. package/distribution/runtime/src/state/components.ts +281 -0
  93. package/distribution/runtime/src/state/graphs.ts +200 -0
  94. package/distribution/runtime/src/state/json.ts +162 -0
  95. package/distribution/runtime/src/state/merge.ts +1034 -0
  96. package/distribution/runtime/src/state/mod.ts +5 -0
  97. package/distribution/runtime/src/state/store.ts +226 -0
  98. package/distribution/runtime/src/state/types.ts +151 -0
  99. package/distribution/runtime/src/studio/client.js +892 -0
  100. package/distribution/runtime/src/studio/components.js +188 -0
  101. package/distribution/runtime/src/studio/index.html +283 -0
  102. package/distribution/runtime/src/studio/layouts.js +170 -0
  103. package/distribution/runtime/src/studio/registry.js +283 -0
  104. package/distribution/runtime/src/studio/relations.js +169 -0
  105. package/distribution/runtime/src/studio/server.ts +461 -0
  106. package/distribution/runtime/src/widgets/deck.ts +39 -0
  107. package/distribution/runtime/src/widgets/plotly.ts +354 -0
  108. package/distribution/runtime/src/widgets/presenter.ts +367 -0
  109. package/distribution/runtime/src/widgets/snapshot.ts +116 -0
  110. package/distribution/runtime/vendor/bento/FONT_NOTICES.md +49 -0
  111. package/distribution/runtime/vendor/bento/LICENSE +21 -0
  112. package/distribution/runtime/vendor/bento/THIRD_PARTY_NOTICES.md +51 -0
  113. package/distribution/runtime/vendor/bento/font-licenses/fraunces-OFL.txt +93 -0
  114. package/distribution/runtime/vendor/bento/font-licenses/instrument-sans-OFL.txt +93 -0
  115. package/distribution/runtime/vendor/bento/pin.json +69 -0
  116. package/distribution/runtime/vendor/bento/provenance/slides.package-lock.json +1928 -0
  117. package/distribution/runtime/vendor/bento/provenance/slides.package.json +28 -0
  118. package/distribution/runtime/vendor/bento/runtime-dependencies.json +244 -0
  119. package/distribution/runtime/vendor/bento/runtime-licenses.json +4038 -0
  120. package/distribution/runtime/vendor/bento/shell.html +269 -0
  121. package/distribution/runtime/vendor/bento/slides.schema.json +2379 -0
  122. package/package.json +40 -4
@@ -0,0 +1,290 @@
1
+ import { originalAttributeTokens } from "./attributes.ts";
2
+ /** Independent proof for the supported literal original-QMD pipe-table subset. */
3
+ import { escapeHtml } from "../render/html.ts";
4
+ import type { PlainTable, TableAlign } from "../render/table.ts";
5
+ import type { Element, Json } from "../render/types.ts";
6
+ import {
7
+ hashSource,
8
+ scanSource,
9
+ type SourceBlock,
10
+ type SourceDocument,
11
+ } from "./mod.ts";
12
+ function fail(message: string): never {
13
+ throw new Error(`UNMAPPED_TABLE_SOURCE: ${message}`);
14
+ }
15
+ function bodyBlocks(doc: SourceDocument, block: SourceBlock) {
16
+ return doc.blocks.filter((b) =>
17
+ b !== block && b.span.start >= block.contentSpan.start &&
18
+ b.span.end <= block.contentSpan.end
19
+ );
20
+ }
21
+ /** Pipes must have explicit outside delimiters; escaped punctuation remains literal. */
22
+ function cells(line: string): string[] {
23
+ let trailingSlashes = 0;
24
+ for (let i = line.length - 2; i >= 0 && line[i] === "\\"; i--) {
25
+ trailingSlashes++;
26
+ }
27
+ if (!line.startsWith("|") || !line.endsWith("|") || trailingSlashes % 2) {
28
+ fail("Use explicit outer pipes for every row.");
29
+ }
30
+ const result: string[] = [];
31
+ let cell = "";
32
+ for (let i = 1; i < line.length - 1; i++) {
33
+ const char = line[i];
34
+ if (
35
+ char === "\\" &&
36
+ /[!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~]/.test(line[i + 1] ?? "")
37
+ ) {
38
+ cell += char + line[++i];
39
+ } else if (char === "|") {
40
+ result.push(cell.trim());
41
+ cell = "";
42
+ } else cell += char;
43
+ }
44
+ result.push(cell.trim());
45
+ return result;
46
+ }
47
+ export function parsePipeTable(content: string): PlainTable {
48
+ const lines = content.replace(/\r\n|\r/g, "\n").replace(/^\n+|\n+$/g, "")
49
+ .split("\n");
50
+ if (
51
+ lines.length < 2 || lines.some((line) => !line.trim() || /^\s/.test(line))
52
+ ) fail("Use one contiguous, unindented pipe table.");
53
+ const header = cells(lines[0]), separator = cells(lines[1]);
54
+ if (
55
+ header.length !== separator.length ||
56
+ !separator.every((cell) => /^:?-{3,}:?$/.test(cell))
57
+ ) {
58
+ fail(
59
+ "The second row must declare one Markdown column separator per header.",
60
+ );
61
+ }
62
+ const columns = separator.map((
63
+ cell,
64
+ ): { width: number; align: TableAlign } => ({
65
+ width: 1,
66
+ align: cell.endsWith(":")
67
+ ? cell.startsWith(":") ? "center" : "right"
68
+ : "left",
69
+ }));
70
+ const rows = [header, ...lines.slice(2).map(cells)].map((row) => {
71
+ if (row.length !== columns.length) fail("Source table is not rectangular.");
72
+ return row.map((cell, index) => ({
73
+ text: cell.replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/g, "$1"),
74
+ align: columns[index].align,
75
+ }));
76
+ });
77
+ return { header: true, columns, rows };
78
+ }
79
+ export function authoredTableBlock(
80
+ doc: SourceDocument,
81
+ wrapperId: string,
82
+ ): SourceBlock {
83
+ if (doc.diagnostics.length) {
84
+ fail("Resolve source mapping diagnostics before rendering tables.");
85
+ }
86
+ const wrappers = doc.blocks.filter((b) =>
87
+ b.id === wrapperId && b.kind === "div" && b.stable && b.explicitId
88
+ );
89
+ if (wrappers.length !== 1) {
90
+ fail("An authored table needs a unique identified fenced-div wrapper.");
91
+ }
92
+ const wrapper = wrappers[0], blocks = bodyBlocks(doc, wrapper);
93
+ if (
94
+ blocks.length !== 1 || blocks[0].kind !== "unsupported" ||
95
+ blocks[0].parentId !== wrapper.id || blocks[0].origin !== "authored"
96
+ ) {
97
+ fail(
98
+ "The identified wrapper must contain exactly one literal pipe table and no computed or other content.",
99
+ );
100
+ }
101
+ parsePipeTable(blocks[0].content);
102
+ return blocks[0];
103
+ }
104
+ /** A native identity binding must be explicit in the original table wrapper. */
105
+ export function tableNativeIdentity(
106
+ doc: SourceDocument,
107
+ wrapperId: string,
108
+ ): string | undefined {
109
+ const original = scanSource(doc.text);
110
+ authoredTableBlock(original, wrapperId);
111
+ const wrapper = original.blocks.find((block) => block.id === wrapperId)!;
112
+ const content = /^ {0,3}:{3,}\s*\{([^{}]*)\}\s*$/.exec(
113
+ wrapper.raw.split(/\r?\n/)[0],
114
+ )?.[1];
115
+ if (content === undefined) {
116
+ fail("Table wrapper requires original literal attributes.");
117
+ }
118
+ const attributes = originalAttributeTokens(content).filter((token) =>
119
+ token.key === "bento-element-id"
120
+ );
121
+ if (attributes.length > 1) {
122
+ fail("Table wrapper has ambiguous native identities.");
123
+ }
124
+ return attributes[0]?.value;
125
+ }
126
+ /** Exact semantic fields, independent of native geometry and table styling. */
127
+ export function literalTableContent(content: string): Record<string, Json> {
128
+ const table = parsePipeTable(content);
129
+ return {
130
+ header: table.header,
131
+ columns: table.columns.map((column) => ({ w: column.width })),
132
+ rows: table.rows.map((row) => ({
133
+ cells: row.map((cell) => ({
134
+ html: escapeHtml(cell.text),
135
+ align: cell.align,
136
+ })),
137
+ })),
138
+ };
139
+ }
140
+ export function tableContent(element: Element): Record<string, Json> {
141
+ return {
142
+ header: element.header,
143
+ columns: element.columns,
144
+ rows: element.rows,
145
+ };
146
+ }
147
+ /** Only the original source bytes may establish a literal table fragment. */
148
+ export function literalTableBlock(
149
+ doc: SourceDocument,
150
+ blockId: string,
151
+ ): SourceBlock {
152
+ if (doc.hash !== hashSource(doc.text)) {
153
+ fail("Original table source snapshot is stale.");
154
+ }
155
+ const original = scanSource(doc.text),
156
+ candidates = original.blocks.filter((b) => b.id === blockId);
157
+ if (
158
+ original.diagnostics.length || candidates.length !== 1 ||
159
+ candidates[0].kind !== "unsupported" || candidates[0].origin !== "authored"
160
+ ) {
161
+ fail("Table has no unique literal original source fragment.");
162
+ }
163
+ const block = candidates[0].parentId
164
+ ? authoredTableBlock(original, candidates[0].parentId)
165
+ : candidates[0];
166
+ if (block.id !== blockId) {
167
+ fail("Original table fragment does not match its container.");
168
+ }
169
+ parsePipeTable(block.content);
170
+ return block;
171
+ }
172
+ export function validateAuthoredTable(
173
+ doc: SourceDocument,
174
+ blockId: string,
175
+ element: Element,
176
+ ): SourceBlock {
177
+ const block = literalTableBlock(doc, blockId);
178
+ if (
179
+ element.type !== "table" ||
180
+ JSON.stringify(tableContent(element)) !==
181
+ JSON.stringify(literalTableContent(block.content))
182
+ ) {
183
+ fail(
184
+ "Executed table cells, header or alignment differ from the exact literal source proof.",
185
+ );
186
+ }
187
+ return block;
188
+ }
189
+ /** Recognize only complete literal top-level tables, never arbitrary unsupported blocks. */
190
+ export function bareTableBlocks(doc: SourceDocument): SourceBlock[] {
191
+ if (doc.hash !== hashSource(doc.text)) {
192
+ fail("Original table source snapshot is stale.");
193
+ }
194
+ const original = scanSource(doc.text);
195
+ if (original.diagnostics.length) {
196
+ fail("Resolve source mapping diagnostics before binding a bare table.");
197
+ }
198
+ return original.blocks.filter((block) => {
199
+ if (
200
+ block.kind !== "unsupported" || block.parentId ||
201
+ block.origin !== "authored"
202
+ ) return false;
203
+ try {
204
+ parsePipeTable(block.content);
205
+ return true;
206
+ } catch {
207
+ return false;
208
+ }
209
+ });
210
+ }
211
+ /** Coverage also applies after an approved wrapper makes a formerly bare table stable. */
212
+ export function literalTableBlocks(doc: SourceDocument): SourceBlock[] {
213
+ if (doc.hash !== hashSource(doc.text)) {
214
+ fail("Original table source snapshot is stale.");
215
+ }
216
+ const original = scanSource(doc.text);
217
+ if (original.diagnostics.length) {
218
+ fail("Resolve source mapping diagnostics before table coverage checks.");
219
+ }
220
+ return original.blocks.filter((block) => {
221
+ if (block.kind !== "unsupported" || block.origin !== "authored") {
222
+ return false;
223
+ }
224
+ try {
225
+ literalTableBlock(original, block.id);
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ });
231
+ }
232
+ /** Bare tables without IDs require a unique exact whole-table source match. */
233
+ export function matchBareTable(
234
+ doc: SourceDocument,
235
+ element: Element,
236
+ ): SourceBlock {
237
+ if (doc.hash !== hashSource(doc.text)) {
238
+ fail("Original table source snapshot is stale.");
239
+ }
240
+ const original = scanSource(doc.text);
241
+ if (original.diagnostics.length) {
242
+ fail("Resolve source mapping diagnostics before binding a bare table.");
243
+ }
244
+ const matches = bareTableBlocks(original).filter((block) => {
245
+ try {
246
+ return JSON.stringify(literalTableContent(block.content)) ===
247
+ JSON.stringify(tableContent(element));
248
+ } catch {
249
+ return false;
250
+ }
251
+ });
252
+ if (matches.length !== 1) {
253
+ fail(
254
+ "A bare table needs one exact original literal table; duplicate or transformed tables require explicit source wrappers.",
255
+ );
256
+ }
257
+ return matches[0];
258
+ }
259
+ export function tableProducer(
260
+ doc: SourceDocument,
261
+ contextId: string,
262
+ verifiedProducerId?: string,
263
+ ): SourceBlock {
264
+ if (doc.hash !== hashSource(doc.text)) fail("Original source hash changed.");
265
+ const original = scanSource(doc.text);
266
+ if (original.diagnostics.length) {
267
+ fail("Resolve source mapping diagnostics before binding a computed table.");
268
+ }
269
+ const contexts = original.blocks.filter((b) => b.id === contextId);
270
+ if (contexts.length !== 1) {
271
+ fail(
272
+ "Computed table does not identify one original producer or container.",
273
+ );
274
+ }
275
+ const context = contexts[0];
276
+ if (
277
+ context.kind === "chunk" &&
278
+ (context.stable || verifiedProducerId === context.id)
279
+ ) return context;
280
+ if (context.kind === "div" && context.stable) {
281
+ const children = bodyBlocks(original, context);
282
+ if (
283
+ children.length === 1 && children[0].kind === "chunk" &&
284
+ children[0].stable
285
+ ) return children[0];
286
+ }
287
+ return fail(
288
+ "Wrap a single explicitly labelled producer chunk in an identified div; a table cannot guess which original computation produced it.",
289
+ );
290
+ }
@@ -0,0 +1,146 @@
1
+ /** Explicit paragraph/list conversion over one independently bound source unit. */
2
+ import {
3
+ scanSource,
4
+ sliceBytes,
5
+ type SourceDocument,
6
+ SourceError,
7
+ type Span,
8
+ } from "./mod.ts";
9
+ import { pureWrapper } from "./deletion.ts";
10
+ import {
11
+ originalAttributeTokens,
12
+ originalPandocAttributes,
13
+ } from "./attributes.ts";
14
+ import { isNativeIdentity } from "./identity-id.ts";
15
+ export interface TextConversion {
16
+ type: "convert-text";
17
+ blockId: string;
18
+ expected: string;
19
+ markdown: string;
20
+ from: "paragraph" | "list";
21
+ to: "paragraph" | "list";
22
+ nativeId: string;
23
+ }
24
+ const fail = (message: string): never => {
25
+ throw new SourceError("unsafe-text-conversion", message);
26
+ };
27
+ function binding(doc: SourceDocument, op: TextConversion) {
28
+ const blocks = doc.blocks.filter((b) => b.id === op.blockId),
29
+ block = blocks[0];
30
+ if (
31
+ doc.diagnostics.length || blocks.length !== 1 || !block.editable ||
32
+ block.origin !== "authored" || block.kind !== op.from ||
33
+ block.content !== op.expected || !block.slideId ||
34
+ !isNativeIdentity(op.nativeId) ||
35
+ !["paragraph", "list"].includes(op.from) ||
36
+ !["paragraph", "list"].includes(op.to) || op.from === op.to
37
+ ) {
38
+ fail(
39
+ "Conversion needs an exact independent authored paragraph/list baseline.",
40
+ );
41
+ }
42
+ const parent = block.parentId
43
+ ? doc.blocks.find((b) => b.id === block.parentId)
44
+ : undefined;
45
+ if (parent) {
46
+ if (!parent.stable || parent.parentId) {
47
+ fail("Nested or weak containers need explicit source authoring.");
48
+ }
49
+ pureWrapper(doc, parent, block);
50
+ if (block.explicitId && block.id !== op.nativeId) {
51
+ fail(
52
+ "The technical marker must retain its native identity during conversion.",
53
+ );
54
+ }
55
+ } else if (!block.stable || block.id !== op.nativeId) {
56
+ fail(
57
+ "Acquire an explicit source/native identity before converting this text.",
58
+ );
59
+ }
60
+ const parsed = scanSource(op.markdown), replacement = parsed.blocks[0];
61
+ if (
62
+ parsed.diagnostics.length || parsed.blocks.length !== 1 ||
63
+ replacement.kind !== op.to || !replacement.editable ||
64
+ replacement.origin !== "authored" || replacement.explicitId ||
65
+ replacement.parentId || replacement.content !== op.markdown ||
66
+ /<!--|-->|\{\{[<%]/.test(op.markdown)
67
+ ) {
68
+ fail(
69
+ "Replacement must be exactly one non-executable authored paragraph or list.",
70
+ );
71
+ }
72
+ return { block, parent };
73
+ }
74
+ export function convertedBlockId(
75
+ doc: SourceDocument,
76
+ op: TextConversion,
77
+ ): string {
78
+ const { block } = binding(doc, op);
79
+ return block.explicitId ??
80
+ (block.parentId ? `${block.parentId}/${op.to}:1` : block.id);
81
+ }
82
+ export function conversionEdits(
83
+ doc: SourceDocument,
84
+ op: TextConversion,
85
+ ): { span: Span; expected: string; replacement: string }[] {
86
+ const { block, parent } = binding(doc, op);
87
+ const edits = [{
88
+ span: block.contentSpan,
89
+ expected: block.content,
90
+ replacement: op.markdown.replace(/\r\n|\r|\n/g, doc.newline),
91
+ }];
92
+ if (parent && !block.explicitId) {
93
+ const opener = parent.raw.split(/\r?\n/)[0];
94
+ const attributeText = /\{([^{}]+)\}/.exec(opener)![1];
95
+ const attributes = originalPandocAttributes(
96
+ attributeText,
97
+ originalAttributeTokens(attributeText),
98
+ );
99
+ const existing = attributes[2].find(([key]) => key === "bento-element-id");
100
+ if (existing && existing[1] !== op.nativeId) {
101
+ fail(
102
+ "Container's explicit native identity differs from the edited object.",
103
+ );
104
+ }
105
+ if (!existing) {
106
+ const end = opener.lastIndexOf("}");
107
+ if (end < 0) fail("Missing verified wrapper attributes.");
108
+ const span = {
109
+ start: parent.span.start,
110
+ end: parent.span.start + new TextEncoder().encode(opener).length,
111
+ };
112
+ edits.push({
113
+ span,
114
+ expected: sliceBytes(doc.text, span),
115
+ replacement: opener.slice(0, end) +
116
+ ` bento-element-id="${op.nativeId}"` + opener.slice(end),
117
+ });
118
+ }
119
+ }
120
+ return edits;
121
+ }
122
+ export function verifyConversion(
123
+ before: SourceDocument,
124
+ after: SourceDocument,
125
+ op: TextConversion,
126
+ ): void {
127
+ const { block, parent } = binding(before, op),
128
+ id = convertedBlockId(before, op),
129
+ next = after.blocks.find((b) => b.id === id);
130
+ if (
131
+ !next || next.kind !== op.to || next.origin !== "authored" ||
132
+ !next.editable || next.slideId !== block.slideId ||
133
+ next.content.replace(/\r\n/g, "\n") !==
134
+ op.markdown.replace(/\r\n/g, "\n") ||
135
+ after.blocks.length !== before.blocks.length
136
+ ) fail("Conversion changed an unrelated source boundary.");
137
+ const prior = before.blocks.filter((b) => b !== block && b !== parent).map(
138
+ (b) => [b.id, b.kind, b.raw, b.origin],
139
+ );
140
+ const remaining = after.blocks.filter((b) =>
141
+ b.id !== id && b.id !== parent?.id
142
+ ).map((b) => [b.id, b.kind, b.raw, b.origin]);
143
+ if (JSON.stringify(prior) !== JSON.stringify(remaining)) {
144
+ fail("Conversion changed unrelated source bytes or identities.");
145
+ }
146
+ }