@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,490 @@
|
|
|
1
|
+
/** Optional descriptive QMD traces; never visual ownership or technical IDs. */
|
|
2
|
+
import {
|
|
3
|
+
hashSource,
|
|
4
|
+
scanSource,
|
|
5
|
+
sliceBytes,
|
|
6
|
+
type SourceDocument,
|
|
7
|
+
SourceError,
|
|
8
|
+
type SourcePatch,
|
|
9
|
+
} from "./mod.ts";
|
|
10
|
+
import {
|
|
11
|
+
originalAttributeTokens,
|
|
12
|
+
originalPandocAttributes,
|
|
13
|
+
} from "./attributes.ts";
|
|
14
|
+
import { canonical } from "../state/json.ts";
|
|
15
|
+
export type AnnotationLevel = "none" | "minimal" | "detailed";
|
|
16
|
+
export type AnnotationFeature =
|
|
17
|
+
| "overrides"
|
|
18
|
+
| "decorations"
|
|
19
|
+
| "stacking"
|
|
20
|
+
| "created-design"
|
|
21
|
+
| "theme"
|
|
22
|
+
| "layout";
|
|
23
|
+
export type AnnotationReference = ".qbento/state.json" | ".qbento/design.json";
|
|
24
|
+
export interface AnnotationRequest {
|
|
25
|
+
slideId: string;
|
|
26
|
+
features: AnnotationFeature[];
|
|
27
|
+
references: AnnotationReference[];
|
|
28
|
+
}
|
|
29
|
+
export interface AnnotationDiagnostic {
|
|
30
|
+
code: string;
|
|
31
|
+
message: string;
|
|
32
|
+
slideId?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface AnnotationPreview {
|
|
35
|
+
version: 1;
|
|
36
|
+
kind: "visual-annotations";
|
|
37
|
+
sourceHash: string;
|
|
38
|
+
level: AnnotationLevel;
|
|
39
|
+
requests: AnnotationRequest[];
|
|
40
|
+
blocked: AnnotationDiagnostic[];
|
|
41
|
+
diagnostics: AnnotationDiagnostic[];
|
|
42
|
+
patches: SourcePatch[];
|
|
43
|
+
resultHash: string;
|
|
44
|
+
result: string;
|
|
45
|
+
}
|
|
46
|
+
const featureNames = new Set([
|
|
47
|
+
"overrides",
|
|
48
|
+
"decorations",
|
|
49
|
+
"stacking",
|
|
50
|
+
"created-design",
|
|
51
|
+
"theme",
|
|
52
|
+
"layout",
|
|
53
|
+
]);
|
|
54
|
+
const references = new Set([".qbento/state.json", ".qbento/design.json"]);
|
|
55
|
+
const encoder = new TextEncoder(),
|
|
56
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
57
|
+
const namespace = "qbento-design";
|
|
58
|
+
function fail(message: string): never {
|
|
59
|
+
throw new SourceError("invalid-annotation", message);
|
|
60
|
+
}
|
|
61
|
+
function normalizedRequests(
|
|
62
|
+
requests: AnnotationRequest[],
|
|
63
|
+
): AnnotationRequest[] {
|
|
64
|
+
const ids = new Set<string>();
|
|
65
|
+
return requests.map((r) => {
|
|
66
|
+
if (
|
|
67
|
+
!r || !/^[\p{L}_][\p{L}\p{N}\p{M}_:@.-]*$/u.test(r.slideId) ||
|
|
68
|
+
ids.has(r.slideId) ||
|
|
69
|
+
!Array.isArray(r.features) || !Array.isArray(r.references) ||
|
|
70
|
+
r.features.some((f) => !featureNames.has(f)) ||
|
|
71
|
+
r.references.some((f) => !references.has(f)) ||
|
|
72
|
+
new Set(r.features).size !== r.features.length ||
|
|
73
|
+
new Set(r.references).size !== r.references.length ||
|
|
74
|
+
!r.features.length || !r.references.length
|
|
75
|
+
) {
|
|
76
|
+
fail(
|
|
77
|
+
"Each trace must identify one stable slide and controlled design references/features.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
ids.add(r.slideId);
|
|
81
|
+
return {
|
|
82
|
+
slideId: r.slideId,
|
|
83
|
+
features: [...r.features].sort(),
|
|
84
|
+
references: [...r.references].sort(),
|
|
85
|
+
};
|
|
86
|
+
}).sort((a, b) => a.slideId.localeCompare(b.slideId));
|
|
87
|
+
}
|
|
88
|
+
function splice(text: string, patches: SourcePatch[]): string {
|
|
89
|
+
let bytes = encoder.encode(text);
|
|
90
|
+
for (const p of [...patches].sort((a, b) => b.span.start - a.span.start)) {
|
|
91
|
+
if (sliceBytes(text, p.span) !== p.expected) {
|
|
92
|
+
throw new SourceError(
|
|
93
|
+
"stale-fragment",
|
|
94
|
+
"Annotation source fragment changed.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const inserted = encoder.encode(p.replacement),
|
|
98
|
+
next = new Uint8Array(
|
|
99
|
+
bytes.length - (p.span.end - p.span.start) + inserted.length,
|
|
100
|
+
);
|
|
101
|
+
next.set(bytes.subarray(0, p.span.start));
|
|
102
|
+
next.set(inserted, p.span.start);
|
|
103
|
+
next.set(bytes.subarray(p.span.end), p.span.start + inserted.length);
|
|
104
|
+
bytes = next;
|
|
105
|
+
}
|
|
106
|
+
return decoder.decode(bytes);
|
|
107
|
+
}
|
|
108
|
+
/** Standalone comments at 0–3 spaces; indented code/tabs and fenced literals are not traces. */
|
|
109
|
+
function comments(doc: SourceDocument) {
|
|
110
|
+
const found: {
|
|
111
|
+
start: number;
|
|
112
|
+
end: number;
|
|
113
|
+
raw: string;
|
|
114
|
+
data: AnnotationRequest;
|
|
115
|
+
}[] = [];
|
|
116
|
+
const yaml = /^(?:\uFEFF)?---[ \t]*\r?\n/.test(doc.text)
|
|
117
|
+
? /^(?:\uFEFF)?---[ \t]*\r?\n[\s\S]*?^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m
|
|
118
|
+
.exec(doc.text)?.[0]
|
|
119
|
+
: undefined;
|
|
120
|
+
const yamlEnd = yaml ? encoder.encode(yaml).length : 0;
|
|
121
|
+
for (
|
|
122
|
+
const match of doc.text.matchAll(
|
|
123
|
+
/^ {0,3}<!--[ \t]*qbento-design\b[^\r\n]*(?:\r\n|\n|$)/gm,
|
|
124
|
+
)
|
|
125
|
+
) {
|
|
126
|
+
const start = encoder.encode(doc.text.slice(0, match.index)).length;
|
|
127
|
+
if (start < yamlEnd) continue;
|
|
128
|
+
if (
|
|
129
|
+
doc.blocks.some((b) =>
|
|
130
|
+
["code", "chunk", "math"].includes(b.kind) && start >= b.span.start &&
|
|
131
|
+
start < b.span.end
|
|
132
|
+
)
|
|
133
|
+
) continue;
|
|
134
|
+
const raw = match[0],
|
|
135
|
+
body =
|
|
136
|
+
/^ {0,3}<!--[ \t]*qbento-design:[ \t]*(\{.*\})[ \t]*-->[ \t]*(?:\r\n|\n|$)$/
|
|
137
|
+
.exec(raw)?.[1];
|
|
138
|
+
let value: {
|
|
139
|
+
version?: unknown;
|
|
140
|
+
slideId?: unknown;
|
|
141
|
+
references?: unknown;
|
|
142
|
+
features?: unknown;
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
if (!body) {
|
|
146
|
+
fail(
|
|
147
|
+
"A reserved qbento-design comment is malformed; preserve and repair it before syncing.",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const keys = [...body.matchAll(/("(?:[^"\\]|\\.)*")[ \t]*:/g)].map((m) =>
|
|
151
|
+
JSON.parse(m[1]) as string
|
|
152
|
+
);
|
|
153
|
+
if (new Set(keys).size !== keys.length) {
|
|
154
|
+
fail(
|
|
155
|
+
"Duplicate keys in a reserved design reference; repair the original comment before sync.",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
value = JSON.parse(body);
|
|
159
|
+
} catch {
|
|
160
|
+
fail(
|
|
161
|
+
"A reserved qbento-design comment is malformed; preserve and repair it before syncing.",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
!value ||
|
|
166
|
+
Object.keys(value).sort().join(",") !==
|
|
167
|
+
"features,references,slideId,version" ||
|
|
168
|
+
value.version !== 1
|
|
169
|
+
) {
|
|
170
|
+
fail(
|
|
171
|
+
"A reserved design reference has an unsupported schema; preserve it rather than guessing its meaning.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const data = normalizedRequests([value as unknown as AnnotationRequest])[0];
|
|
175
|
+
found.push({ start, end: start + encoder.encode(raw).length, raw, data });
|
|
176
|
+
}
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
export interface SourceDesignAnnotation {
|
|
180
|
+
slideId: string;
|
|
181
|
+
/** Minimal classes deliberately do not identify one specific sidecar. */
|
|
182
|
+
features?: AnnotationFeature[];
|
|
183
|
+
references?: AnnotationReference[];
|
|
184
|
+
}
|
|
185
|
+
/** Read original managed traces only; no patches, technical IDs or ownership. */
|
|
186
|
+
export function readDesignAnnotations(
|
|
187
|
+
doc: SourceDocument,
|
|
188
|
+
): SourceDesignAnnotation[] {
|
|
189
|
+
if (hashSource(doc.text) !== doc.hash) {
|
|
190
|
+
throw new SourceError("stale-document", "Source snapshot changed.");
|
|
191
|
+
}
|
|
192
|
+
const existing = comments(doc), consumed = new Set<number>();
|
|
193
|
+
const result: SourceDesignAnnotation[] = [];
|
|
194
|
+
for (const slide of doc.slides) {
|
|
195
|
+
const heading = doc.blocks.find((b) => b.id === slide.headingId)!;
|
|
196
|
+
const associated = existing.filter((c) =>
|
|
197
|
+
c.start >= heading.span.end &&
|
|
198
|
+
/^[\r\n \t]*$/.test(
|
|
199
|
+
sliceBytes(doc.text, { start: heading.span.end, end: c.start }),
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
if (associated.length > 1) {
|
|
203
|
+
fail("More than one descriptive reference follows a slide heading.");
|
|
204
|
+
}
|
|
205
|
+
const current = associated[0];
|
|
206
|
+
if (current) {
|
|
207
|
+
consumed.add(current.start);
|
|
208
|
+
if (current.data.slideId !== slide.id) {
|
|
209
|
+
fail(
|
|
210
|
+
"A descriptive reference points to a different source slide; preserve and reconcile it before sync.",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const line = heading.raw.replace(/(?:\r\n|\n|\r)$/, "");
|
|
215
|
+
const attrs = /\{([^{}]*)\}[ \t]*$/.exec(line);
|
|
216
|
+
let annotated = false;
|
|
217
|
+
if (
|
|
218
|
+
attrs?.[1].replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, "")
|
|
219
|
+
.match(/(?:^|\s)\.qbento-design(?:\s|$)/)
|
|
220
|
+
) {
|
|
221
|
+
const tokens = originalAttributeTokens(attrs[1]);
|
|
222
|
+
originalPandocAttributes(attrs[1], tokens);
|
|
223
|
+
const marks = tokens.filter((t) =>
|
|
224
|
+
attrs[1].slice(t.start, t.end) === `.${namespace}`
|
|
225
|
+
);
|
|
226
|
+
if (marks.length > 1) {
|
|
227
|
+
fail(
|
|
228
|
+
"Duplicate .qbento-design class; preserve and repair it before sync.",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
annotated = marks.length === 1;
|
|
232
|
+
}
|
|
233
|
+
if (annotated || current) {
|
|
234
|
+
result.push({
|
|
235
|
+
slideId: slide.id,
|
|
236
|
+
...(current
|
|
237
|
+
? {
|
|
238
|
+
features: current.data.features,
|
|
239
|
+
references: current.data.references,
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (existing.some((c) => !consumed.has(c.start))) {
|
|
246
|
+
fail(
|
|
247
|
+
"A reserved design reference does not directly follow its source slide heading.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
/** Patches exactly optional class tokens/reference comments, preserving all other bytes. */
|
|
253
|
+
export function previewAnnotations(
|
|
254
|
+
document: SourceDocument,
|
|
255
|
+
level: AnnotationLevel,
|
|
256
|
+
requests: AnnotationRequest[],
|
|
257
|
+
blocked: AnnotationDiagnostic[] = [],
|
|
258
|
+
): AnnotationPreview {
|
|
259
|
+
if (!["none", "minimal", "detailed"].includes(level)) {
|
|
260
|
+
fail("Unknown annotation level.");
|
|
261
|
+
}
|
|
262
|
+
if (hashSource(document.text) !== document.hash) {
|
|
263
|
+
throw new SourceError("stale-document", "Source snapshot changed.");
|
|
264
|
+
}
|
|
265
|
+
const doc = scanSource(document.text), desired = normalizedRequests(requests);
|
|
266
|
+
if (doc.diagnostics.length) {
|
|
267
|
+
throw new SourceError(
|
|
268
|
+
"invalid-source-structure",
|
|
269
|
+
"Repair QMD source diagnostics before annotation sync.",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const existing = comments(doc), consumed = new Set<number>();
|
|
273
|
+
const patches: SourcePatch[] = [], diagnostics: AnnotationDiagnostic[] = [];
|
|
274
|
+
for (const slide of doc.slides) {
|
|
275
|
+
const heading = doc.blocks.find((b) => b.id === slide.headingId)!;
|
|
276
|
+
const request = desired.find((r) => r.slideId === slide.id);
|
|
277
|
+
const line = heading.raw.replace(/(?:\r\n|\n|\r)$/, "");
|
|
278
|
+
const attrs = /\{([^{}]*)\}[ \t]*$/.exec(line);
|
|
279
|
+
const associated = existing.filter((c) =>
|
|
280
|
+
c.start >= heading.span.end &&
|
|
281
|
+
/^[\r\n \t]*$/.test(
|
|
282
|
+
sliceBytes(doc.text, { start: heading.span.end, end: c.start }),
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
if (associated.length > 1) {
|
|
286
|
+
fail("More than one descriptive reference follows a slide heading.");
|
|
287
|
+
}
|
|
288
|
+
const current = associated[0];
|
|
289
|
+
if (current) {
|
|
290
|
+
consumed.add(current.start);
|
|
291
|
+
if (current.data.slideId !== slide.id) {
|
|
292
|
+
fail(
|
|
293
|
+
"A descriptive reference points to a different source slide; preserve and reconcile it before sync.",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
!current &&
|
|
299
|
+
!attrs?.[1].replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, "").match(
|
|
300
|
+
/(?:^|\s)\.qbento-design(?:\s|$)/,
|
|
301
|
+
) &&
|
|
302
|
+
(level === "none" || !request)
|
|
303
|
+
) continue;
|
|
304
|
+
let parsedId: string | undefined;
|
|
305
|
+
let annotated = false;
|
|
306
|
+
const local: {
|
|
307
|
+
start: number;
|
|
308
|
+
end: number;
|
|
309
|
+
expected: string;
|
|
310
|
+
replacement: string;
|
|
311
|
+
}[] = [];
|
|
312
|
+
if (attrs) {
|
|
313
|
+
let tokens: ReturnType<typeof originalAttributeTokens>;
|
|
314
|
+
try {
|
|
315
|
+
tokens = originalAttributeTokens(attrs[1]);
|
|
316
|
+
parsedId = originalPandocAttributes(attrs[1], tokens)[0];
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (!(error instanceof SourceError)) throw error;
|
|
319
|
+
diagnostics.push({
|
|
320
|
+
code: "ANNOTATION_UNSUPPORTED_ATTRIBUTES",
|
|
321
|
+
slideId: slide.id,
|
|
322
|
+
message:
|
|
323
|
+
"Optional annotation update omitted: this heading uses attribute syntax outside the bounded original-source parser. Existing attributes, technical IDs and pending semantic edits are preserved.",
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const marks = tokens.filter((t) =>
|
|
328
|
+
attrs[1].slice(t.start, t.end) === `.${namespace}`
|
|
329
|
+
);
|
|
330
|
+
if (marks.length > 1) {
|
|
331
|
+
fail(
|
|
332
|
+
"Duplicate .qbento-design class; preserve and repair it before sync.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
annotated = marks.length === 1;
|
|
336
|
+
const wanted = level !== "none" && !!request;
|
|
337
|
+
if (annotated && !wanted) {
|
|
338
|
+
const mark = marks[0];
|
|
339
|
+
const start = mark.start && attrs[1][mark.start - 1] === " "
|
|
340
|
+
? mark.start - 1
|
|
341
|
+
: mark.start;
|
|
342
|
+
local.push({
|
|
343
|
+
start: attrs.index + 1 + start,
|
|
344
|
+
end: attrs.index + 1 + mark.end,
|
|
345
|
+
expected: attrs[1].slice(start, mark.end),
|
|
346
|
+
replacement: "",
|
|
347
|
+
});
|
|
348
|
+
} else if (!annotated && wanted) {
|
|
349
|
+
local.push({
|
|
350
|
+
start: attrs.index + 1 + attrs[1].length,
|
|
351
|
+
end: attrs.index + 1 + attrs[1].length,
|
|
352
|
+
expected: "",
|
|
353
|
+
replacement: ` .${namespace}`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
} else if (level !== "none" && request) {
|
|
357
|
+
diagnostics.push({
|
|
358
|
+
code: "ANNOTATION_UNPINNED_SLIDE",
|
|
359
|
+
slideId: slide.id,
|
|
360
|
+
message:
|
|
361
|
+
"Optional design signal omitted: use an existing explicit heading attribute ID; annotation sync never pins technical identity implicitly.",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const pinned = slide.stable && !!attrs && parsedId === slide.id;
|
|
365
|
+
if ((local.length || current || request) && !pinned) {
|
|
366
|
+
if (local.length || current) {
|
|
367
|
+
diagnostics.push({
|
|
368
|
+
code: "ANNOTATION_UNPINNED_SLIDE",
|
|
369
|
+
slideId: slide.id,
|
|
370
|
+
message:
|
|
371
|
+
"Optional annotation update omitted: the slide needs an existing explicit heading ID; technical markers and IDs are retained.",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
for (const p of local) {
|
|
377
|
+
patches.push({
|
|
378
|
+
blockId: slide.id,
|
|
379
|
+
span: {
|
|
380
|
+
start: heading.span.start +
|
|
381
|
+
encoder.encode(line.slice(0, p.start)).length,
|
|
382
|
+
end: heading.span.start + encoder.encode(line.slice(0, p.end)).length,
|
|
383
|
+
},
|
|
384
|
+
expected: p.expected,
|
|
385
|
+
replacement: p.replacement,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const wantedComment = level === "detailed" && request
|
|
389
|
+
? `<!-- ${namespace}: ${
|
|
390
|
+
canonical({ version: 1, ...request })
|
|
391
|
+
} -->${doc.newline}`
|
|
392
|
+
: undefined;
|
|
393
|
+
if (current) {
|
|
394
|
+
const replacement = wantedComment ?? "";
|
|
395
|
+
if (current.raw !== replacement) {
|
|
396
|
+
patches.push({
|
|
397
|
+
blockId: slide.id,
|
|
398
|
+
span: { start: current.start, end: current.end },
|
|
399
|
+
expected: current.raw,
|
|
400
|
+
replacement,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} else if (wantedComment) {
|
|
404
|
+
const separator = /[\r\n]$/.test(heading.raw) ? "" : doc.newline;
|
|
405
|
+
patches.push({
|
|
406
|
+
blockId: slide.id,
|
|
407
|
+
span: { start: heading.span.end, end: heading.span.end },
|
|
408
|
+
expected: "",
|
|
409
|
+
replacement: separator + wantedComment,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (existing.some((c) => !consumed.has(c.start))) {
|
|
414
|
+
fail(
|
|
415
|
+
"Reserved design reference is not directly associated with one original slide heading.",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
for (const r of desired) {
|
|
419
|
+
if (level !== "none" && !doc.slides.some((s) => s.id === r.slideId)) {
|
|
420
|
+
diagnostics.push({
|
|
421
|
+
code: "ANNOTATION_UNMAPPED_SLIDE",
|
|
422
|
+
slideId: r.slideId,
|
|
423
|
+
message:
|
|
424
|
+
"Optional trace omitted: no matching original source slide exists. Add an explicit original heading ID, recalculate, and review; annotation sync never pins identity automatically.",
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
patches.sort((a, b) => a.span.start - b.span.start);
|
|
429
|
+
for (let i = 1; i < patches.length; i++) {
|
|
430
|
+
if (patches[i].span.start < patches[i - 1].span.end) {
|
|
431
|
+
fail("Annotation patches overlap.");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const applied = blocked.length ? [] : patches;
|
|
435
|
+
const result = splice(doc.text, applied), rescanned = scanSource(result);
|
|
436
|
+
const shape = (d: SourceDocument) =>
|
|
437
|
+
d.blocks.map((
|
|
438
|
+
b,
|
|
439
|
+
) => [
|
|
440
|
+
b.kind,
|
|
441
|
+
b.explicitId,
|
|
442
|
+
b.level,
|
|
443
|
+
b.origin,
|
|
444
|
+
...(b.kind === "div" ? [] : [b.content]),
|
|
445
|
+
]);
|
|
446
|
+
if (
|
|
447
|
+
canonical(shape(doc)) !== canonical(shape(rescanned)) ||
|
|
448
|
+
rescanned.diagnostics.length
|
|
449
|
+
) {
|
|
450
|
+
fail(
|
|
451
|
+
"Optional annotation changed semantic content, identity or source structure.",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
version: 1,
|
|
456
|
+
kind: "visual-annotations",
|
|
457
|
+
sourceHash: doc.hash,
|
|
458
|
+
level,
|
|
459
|
+
requests: desired,
|
|
460
|
+
blocked,
|
|
461
|
+
diagnostics: [...diagnostics, ...blocked],
|
|
462
|
+
patches: applied,
|
|
463
|
+
resultHash: hashSource(result),
|
|
464
|
+
result,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
export function applyAnnotations(
|
|
468
|
+
text: string,
|
|
469
|
+
plan: AnnotationPreview,
|
|
470
|
+
): string {
|
|
471
|
+
if (hashSource(text) !== plan.sourceHash) {
|
|
472
|
+
throw new SourceError(
|
|
473
|
+
"stale-document",
|
|
474
|
+
"Source changed since annotation preview.",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const fresh = previewAnnotations(
|
|
478
|
+
scanSource(text),
|
|
479
|
+
plan.level,
|
|
480
|
+
plan.requests,
|
|
481
|
+
plan.blocked,
|
|
482
|
+
);
|
|
483
|
+
if (canonical(fresh) !== canonical(plan)) {
|
|
484
|
+
throw new SourceError(
|
|
485
|
+
"invalid-preview",
|
|
486
|
+
"Annotation preview was changed or cannot be reproduced.",
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
return fresh.result;
|
|
490
|
+
}
|