@openpresentation/opf-editor 0.10.5 → 0.11.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.
Files changed (76) hide show
  1. package/README.md +333 -8
  2. package/dist/annotations.d.ts +71 -0
  3. package/dist/annotations.js +281 -0
  4. package/dist/assets.d.ts +67 -0
  5. package/dist/assets.js +176 -0
  6. package/dist/background-options.d.ts +48 -0
  7. package/dist/background-options.js +134 -0
  8. package/dist/block-convert.d.ts +64 -0
  9. package/dist/block-convert.js +142 -0
  10. package/dist/canvas.d.ts +16 -0
  11. package/dist/canvas.js +82 -21
  12. package/dist/chart-data.d.ts +32 -0
  13. package/dist/chart-data.js +101 -0
  14. package/dist/chart-options-panel.d.ts +16 -0
  15. package/dist/chart-options-panel.js +127 -0
  16. package/dist/chart-options.d.ts +49 -0
  17. package/dist/chart-options.js +157 -0
  18. package/dist/content-actions.d.ts +91 -0
  19. package/dist/content-actions.js +207 -0
  20. package/dist/content-controls.js +326 -0
  21. package/dist/data-grid.d.ts +37 -0
  22. package/dist/data-grid.js +1035 -0
  23. package/dist/design-controls.d.ts +43 -0
  24. package/dist/design-controls.js +1077 -0
  25. package/dist/design-options.d.ts +108 -0
  26. package/dist/design-options.js +412 -0
  27. package/dist/edit-helpers.js +52 -0
  28. package/dist/export.d.ts +77 -0
  29. package/dist/export.js +216 -0
  30. package/dist/find-panel.d.ts +44 -0
  31. package/dist/find-panel.js +431 -0
  32. package/dist/find-replace.d.ts +100 -0
  33. package/dist/find-replace.js +374 -0
  34. package/dist/grid-model.d.ts +135 -0
  35. package/dist/grid-model.js +836 -0
  36. package/dist/grid-text.d.ts +33 -0
  37. package/dist/grid-text.js +251 -0
  38. package/dist/image-crop.d.ts +59 -0
  39. package/dist/image-crop.js +336 -0
  40. package/dist/image-cropper.d.ts +29 -0
  41. package/dist/image-cropper.js +519 -0
  42. package/dist/index.d.ts +11 -1
  43. package/dist/index.js +104 -171
  44. package/dist/numbering-panel.d.ts +21 -0
  45. package/dist/numbering-panel.js +200 -0
  46. package/dist/numbering.d.ts +62 -0
  47. package/dist/numbering.js +223 -0
  48. package/dist/outline-view.d.ts +17 -0
  49. package/dist/outline-view.js +278 -0
  50. package/dist/outline.d.ts +56 -0
  51. package/dist/outline.js +271 -0
  52. package/dist/persistence-ui.d.ts +24 -0
  53. package/dist/persistence-ui.js +81 -0
  54. package/dist/persistence.d.ts +105 -0
  55. package/dist/persistence.js +429 -0
  56. package/dist/review-panel.d.ts +44 -0
  57. package/dist/review-panel.js +359 -0
  58. package/dist/review.d.ts +75 -0
  59. package/dist/review.js +170 -0
  60. package/dist/slide-manager.d.ts +44 -0
  61. package/dist/slide-manager.js +695 -0
  62. package/dist/slides.d.ts +96 -0
  63. package/dist/slides.js +433 -0
  64. package/dist/switches.d.ts +26 -0
  65. package/dist/switches.js +127 -43
  66. package/dist/table-options.d.ts +80 -0
  67. package/dist/table-options.js +419 -0
  68. package/dist/table-structure.d.ts +30 -0
  69. package/dist/table-structure.js +92 -0
  70. package/dist/template-panel.d.ts +31 -0
  71. package/dist/template-panel.js +377 -0
  72. package/dist/templates.d.ts +126 -0
  73. package/dist/templates.js +331 -0
  74. package/dist/zip.d.ts +4 -0
  75. package/dist/zip.js +71 -0
  76. package/package.json +150 -10
@@ -0,0 +1,377 @@
1
+ // Fill template panel (RR-32): the DOM panel over the template model. It lists the document's variables with typed
2
+ // inputs (text, number, date, color, URL, list, and an image source with an asset pick or an uploaded file), shows
3
+ // which are filled and which still need a value, previews the result as values change, and fills the deck as one
4
+ // undoable edit. A second section inserts a variable token into the host's selected text (declaring a new variable
5
+ // in the same edit when asked). The panel owns no document state: values live in the fill session until applied.
6
+ // Importing this module does not need a DOM; mounting does.
7
+ import {
8
+ createTemplateFill,
9
+ insertVariableToken,
10
+ isTemplateDocument,
11
+ setTemplate,
12
+ suggestVariableId,
13
+ templatesAvailable,
14
+ } from "./templates.js";
15
+
16
+ const KIND_LABELS = { text: "Text", number: "Number", date: "Date", image: "Image", url: "Link", list: "List", color: "Color" };
17
+ const STATUS_TEXT = { filled: "Filled", default: "Default value", unfilled: "Needs a value", optional: "Optional" };
18
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
19
+ const NEW_VARIABLE_KINDS = ["text", "number", "date", "image", "url", "list"];
20
+
21
+ let panelCounter = 0;
22
+
23
+ function h(doc, tag, attributes = {}, ...children) {
24
+ const element = doc.createElement(tag);
25
+ for (const [key, value] of Object.entries(attributes)) {
26
+ if (value === undefined || value === false) continue;
27
+ if (key === "class") element.className = value;
28
+ else if (key === "text") element.textContent = value;
29
+ else if (key.startsWith("on")) element.addEventListener(key.slice(2), value);
30
+ else element.setAttribute(key, value === true ? "" : String(value));
31
+ }
32
+ for (const child of children) if (child) element.append(child);
33
+ return element;
34
+ }
35
+
36
+ /**
37
+ * Mount the Fill template panel. Options: `editor` (a session), `renderPreview({document, variables, slideIndex})`
38
+ * returning an SVG string (or a promise of one) for the live preview (omit for no preview), `getSlideIndex`,
39
+ * `getTarget()` returning `{path, start?, end?}` for the text field a token is inserted into, `onStatus(message,
40
+ * {error})`, `onApply(result)`, `readFile(file)` (default: FileReader as a data URL) for image uploads.
41
+ */
42
+ export function createTemplatePanel(container, options) {
43
+ if (!templatesAvailable()) throw new Error("The Fill template panel needs a core release that ships resolveVariables.");
44
+ const { editor, renderPreview, getTarget, onStatus, onApply } = options;
45
+ const doc = container.ownerDocument;
46
+ const fill = createTemplateFill(editor);
47
+ const id = `opf-template-${++panelCounter}`;
48
+ let previewSlide = options.getSlideIndex?.() ?? 0;
49
+ let previewToken = 0;
50
+ let destroyed = false;
51
+ const rows = new Map();
52
+
53
+ const root = h(doc, "section", { class: "opf-template-panel", "data-opf-component": "template-panel", "aria-label": "Fill template" });
54
+ const summary = h(doc, "p", { class: "opf-template-summary", id: `${id}-summary` });
55
+ const modeLabel = h(doc, "label", { class: "opf-template-mode" });
56
+ const modeBox = h(doc, "input", { type: "checkbox", id: `${id}-mode` });
57
+ modeLabel.append(modeBox, " This presentation is a template (it may have unfilled variables)");
58
+ const fieldList = h(doc, "div", { class: "opf-template-fields" });
59
+ const live = h(doc, "p", { class: "opf-template-live", role: "status", "aria-live": "polite" });
60
+ const applyButton = h(doc, "button", { type: "button", class: "opf-template-apply primary", text: "Fill the presentation" });
61
+ const partialButton = h(doc, "button", { type: "button", class: "opf-template-partial secondary", text: "Fill what is ready" });
62
+ const resetButton = h(doc, "button", { type: "button", class: "opf-template-reset quiet", text: "Clear values" });
63
+ const actions = h(doc, "div", { class: "opf-template-actions" }, applyButton, partialButton, resetButton);
64
+ const slideSelect = h(doc, "select", { id: `${id}-slide` });
65
+ const previewStatus = h(doc, "p", { class: "opf-template-preview-status", role: "status" });
66
+ const previewBox = h(doc, "div", { class: "opf-template-preview", "aria-label": "Preview with these values" });
67
+ const previewSection = renderPreview ? h(doc, "div", { class: "opf-template-preview-section" }, h(doc, "label", { for: `${id}-slide`, text: "Preview" }), slideSelect, previewStatus, previewBox) : null;
68
+
69
+ // Insert a variable into the selected text.
70
+ const insertSelect = h(doc, "select", { id: `${id}-insert-variable` });
71
+ const insertButton = h(doc, "button", { type: "button", class: "opf-template-insert secondary", text: "Insert into selected text" });
72
+ const newName = h(doc, "input", { type: "text", id: `${id}-new-name`, placeholder: "e.g. Client name" });
73
+ const newKind = h(doc, "select", { id: `${id}-new-kind` }, ...NEW_VARIABLE_KINDS.map((kind) => h(doc, "option", { value: kind, text: KIND_LABELS[kind] })));
74
+ const newSample = h(doc, "input", { type: "text", id: `${id}-new-sample`, placeholder: "Sample or current value" });
75
+ const newButton = h(doc, "button", { type: "button", class: "opf-template-new secondary", text: "Create and insert" });
76
+ const insertSection = getTarget
77
+ ? h(doc, "details", { class: "opf-template-insert-section" },
78
+ h(doc, "summary", { text: "Insert a variable into text" }),
79
+ h(doc, "p", { class: "opf-template-help", text: "Select a text field on the slide, then choose a variable. The token {{name}} is replaced by its value when the presentation is filled." }),
80
+ h(doc, "label", { for: `${id}-insert-variable`, text: "Variable" }), insertSelect, insertButton,
81
+ h(doc, "fieldset", { class: "opf-template-new-variable" },
82
+ h(doc, "legend", { text: "New variable" }),
83
+ h(doc, "label", { for: `${id}-new-name`, text: "Name" }), newName,
84
+ h(doc, "label", { for: `${id}-new-kind`, text: "Kind" }), newKind,
85
+ h(doc, "label", { for: `${id}-new-sample`, text: "Sample value" }), newSample, newButton))
86
+ : null;
87
+
88
+ root.append(summary, modeLabel, fieldList, actions, live);
89
+ if (previewSection) root.append(previewSection);
90
+ if (insertSection) root.append(insertSection);
91
+ container.append(root);
92
+
93
+ const say = (message, error = false) => {
94
+ live.textContent = message;
95
+ live.dataset.error = error ? "true" : "false";
96
+ onStatus?.(message, { error });
97
+ };
98
+ const messageOf = (error) => error?.issues?.[0]?.message ?? error?.message ?? String(error);
99
+
100
+ function readUpload(file) {
101
+ if (options.readFile) return Promise.resolve(options.readFile(file));
102
+ return new Promise((resolve, reject) => {
103
+ const reader = new (doc.defaultView.FileReader)();
104
+ reader.onload = () => resolve(String(reader.result));
105
+ reader.onerror = () => reject(new Error("The file could not be read."));
106
+ reader.readAsDataURL(file);
107
+ });
108
+ }
109
+
110
+ function control(field) {
111
+ const base = { id: `${id}-field-${field.id}`, "aria-describedby": `${id}-help-${field.id} ${id}-error-${field.id}` };
112
+ let input;
113
+ switch (field.kind) {
114
+ case "list":
115
+ input = h(doc, "textarea", { ...base, rows: 4, placeholder: field.placeholder });
116
+ break;
117
+ case "date":
118
+ input = h(doc, "input", { ...base, type: "date" });
119
+ break;
120
+ case "url":
121
+ input = h(doc, "input", { ...base, type: "url", placeholder: field.placeholder, inputmode: "url" });
122
+ break;
123
+ case "number":
124
+ input = h(doc, "input", { ...base, type: "text", inputmode: "decimal", placeholder: field.placeholder });
125
+ break;
126
+ case "color":
127
+ input = h(doc, "input", { ...base, type: "text", placeholder: field.placeholder || "#0F4C81", maxlength: 9 });
128
+ break;
129
+ default:
130
+ input = h(doc, "input", { ...base, type: "text", placeholder: field.placeholder });
131
+ }
132
+ input.value = field.text;
133
+ return input;
134
+ }
135
+
136
+ function buildRow(field) {
137
+ const input = control(field);
138
+ const error = h(doc, "p", { class: "opf-template-error", id: `${id}-error-${field.id}`, role: "alert", hidden: true });
139
+ const status = h(doc, "span", { class: "opf-template-status" });
140
+ const help = h(doc, "p", { class: "opf-template-help", id: `${id}-help-${field.id}` });
141
+ const useDefault = h(doc, "button", { type: "button", class: "opf-template-default quiet", text: "Use default" });
142
+ const extras = [];
143
+ let colorInput;
144
+ let assetSelect;
145
+ let fileInput;
146
+ if (field.kind === "color") {
147
+ colorInput = h(doc, "input", { type: "color", "aria-label": `${field.label} color picker` });
148
+ colorInput.addEventListener("input", () => {
149
+ input.value = colorInput.value.toUpperCase();
150
+ commit();
151
+ });
152
+ extras.push(colorInput);
153
+ }
154
+ if (field.kind === "image") {
155
+ const assets = editor.document.assets && typeof editor.document.assets === "object" ? Object.keys(editor.document.assets) : [];
156
+ if (assets.length) {
157
+ assetSelect = h(doc, "select", { "aria-label": `${field.label}: pick a registered asset` }, h(doc, "option", { value: "", text: "Pick an asset…" }), ...assets.map((key) => h(doc, "option", { value: `asset:${key}`, text: key })));
158
+ assetSelect.addEventListener("change", () => {
159
+ if (!assetSelect.value) return;
160
+ input.value = assetSelect.value;
161
+ commit();
162
+ assetSelect.value = "";
163
+ });
164
+ extras.push(assetSelect);
165
+ }
166
+ fileInput = h(doc, "input", { type: "file", accept: "image/*", "aria-label": `${field.label}: upload an image` });
167
+ fileInput.addEventListener("change", async () => {
168
+ const file = fileInput.files?.[0];
169
+ if (!file) return;
170
+ if (file.size > MAX_IMAGE_BYTES) {
171
+ showError(`That image is ${(file.size / 1048576).toFixed(1)} MB; the limit is 5 MB.`);
172
+ return;
173
+ }
174
+ try {
175
+ input.value = await readUpload(file);
176
+ commit();
177
+ } catch (failure) {
178
+ showError(messageOf(failure));
179
+ }
180
+ });
181
+ extras.push(fileInput);
182
+ }
183
+ const label = h(doc, "label", { for: input.id }, field.label, field.required ? h(doc, "span", { class: "opf-required", "aria-hidden": "true", text: " *" }) : null, h(doc, "span", { class: "opf-template-kind", text: ` (${KIND_LABELS[field.kind]})` }));
184
+ const element = h(doc, "div", { class: "opf-template-field", "data-variable": field.id, "data-kind": field.kind }, label, status, input, ...extras, help, error, useDefault);
185
+
186
+ function showError(message) {
187
+ error.hidden = !message;
188
+ error.textContent = message ?? "";
189
+ input.setAttribute("aria-invalid", message ? "true" : "false");
190
+ }
191
+ function commit() {
192
+ const parsed = fill.setText(field.id, input.value);
193
+ showError(parsed.ok ? undefined : parsed.message);
194
+ }
195
+ input.addEventListener("input", commit);
196
+ useDefault.addEventListener("click", () => {
197
+ input.value = "";
198
+ fill.clear(field.id);
199
+ showError(undefined);
200
+ });
201
+ return { element, input, status, help, useDefault, colorInput, showError };
202
+ }
203
+
204
+ function rebuild() {
205
+ fill.prune();
206
+ const fields = fill.fields();
207
+ rows.clear();
208
+ fieldList.replaceChildren();
209
+ if (!fields.length) {
210
+ fieldList.append(h(doc, "p", { class: "opf-template-empty", text: "This presentation has no variables yet. Declare variables in the source, or insert one from a text field below." }));
211
+ }
212
+ for (const field of fields) {
213
+ const row = buildRow(field);
214
+ rows.set(field.id, row);
215
+ fieldList.append(row.element);
216
+ }
217
+ insertSelect.replaceChildren(...fields.map((field) => h(doc, "option", { value: field.id, text: `${field.label} (${KIND_LABELS[field.kind]})` })));
218
+ slideSelect.replaceChildren(...(editor.document.slides ?? []).map((slide, index) => h(doc, "option", { value: String(index), text: `${index + 1}. ${typeof slide.title === "string" && slide.title ? slide.title : "Untitled"}` })));
219
+ previewSlide = Math.min(previewSlide, Math.max(0, (editor.document.slides?.length ?? 1) - 1));
220
+ slideSelect.value = String(previewSlide);
221
+ modeBox.checked = isTemplateDocument(editor.document);
222
+ update();
223
+ }
224
+
225
+ function update() {
226
+ const fields = fill.fields();
227
+ const status = fill.status();
228
+ for (const field of fields) {
229
+ const row = rows.get(field.id);
230
+ if (!row) continue;
231
+ row.element.dataset.status = field.status;
232
+ row.status.textContent = STATUS_TEXT[field.status];
233
+ row.useDefault.hidden = field.status !== "filled";
234
+ const uses = field.uses.length;
235
+ row.help.textContent = [
236
+ field.description,
237
+ uses ? `Used in ${uses} place${uses === 1 ? "" : "s"}.` : "Not used anywhere yet.",
238
+ field.kind === "list" ? "One entry per line." : undefined,
239
+ field.kind === "number" ? "Digits only, no thousands separators." : undefined,
240
+ field.kind === "date" ? "A calendar date." : undefined,
241
+ field.rich ? "Rich text: its formatting stays until you type a replacement." : undefined,
242
+ field.format ? `Shown as ${field.format}.` : undefined,
243
+ ].filter(Boolean).join(" ");
244
+ if (row.colorInput) {
245
+ const hex = /^#[0-9a-fA-F]{6}$/.test(row.input.value) ? row.input.value : /^#[0-9a-fA-F]{6}$/.test(field.placeholder) ? field.placeholder : "#000000";
246
+ row.colorInput.value = hex.toLowerCase();
247
+ }
248
+ }
249
+ const waiting = status.unfilled.length;
250
+ summary.textContent = status.fieldCount
251
+ ? waiting
252
+ ? `${status.filledRequiredCount} of ${status.requiredCount} required variables filled. Still needed: ${status.unfilled.join(", ")}.`
253
+ : `All ${status.requiredCount} required variables have a value.`
254
+ : "No variables.";
255
+ applyButton.disabled = !status.fieldCount || waiting > 0;
256
+ partialButton.disabled = !status.fieldCount || waiting === 0 || waiting === status.fieldCount;
257
+ resetButton.disabled = Object.keys(fill.values).length === 0;
258
+ insertButton.disabled = !insertSelect.options.length;
259
+ drawPreview();
260
+ }
261
+
262
+ async function drawPreview() {
263
+ if (!renderPreview) return;
264
+ const token = ++previewToken;
265
+ try {
266
+ const svg = await renderPreview({ document: editor.document, variables: fill.values, slideIndex: previewSlide });
267
+ if (token !== previewToken || destroyed) return;
268
+ previewBox.innerHTML = svg;
269
+ const preview = fill.preview();
270
+ const sample = preview.examplesUsed.length;
271
+ previewStatus.textContent = sample ? `Previewing with example values for: ${preview.examplesUsed.join(", ")}.` : "";
272
+ } catch (error) {
273
+ if (token !== previewToken || destroyed) return;
274
+ previewBox.replaceChildren();
275
+ previewStatus.textContent = `Preview unavailable: ${messageOf(error)}`;
276
+ }
277
+ }
278
+
279
+ async function apply(partial) {
280
+ try {
281
+ const result = fill.apply({ partial });
282
+ say(partial && !result.complete ? `Filled what was ready. Still needed: ${result.unfilled.join(", ")}.` : "Filled the presentation. Undo restores the template.");
283
+ onApply?.(result);
284
+ } catch (error) {
285
+ say(messageOf(error), true);
286
+ }
287
+ }
288
+ applyButton.addEventListener("click", () => apply(false));
289
+ partialButton.addEventListener("click", () => apply(true));
290
+ resetButton.addEventListener("click", () => {
291
+ fill.reset();
292
+ for (const row of rows.values()) {
293
+ row.input.value = "";
294
+ row.showError(undefined);
295
+ }
296
+ say("Cleared the values.");
297
+ });
298
+ slideSelect.addEventListener("change", () => {
299
+ previewSlide = Number(slideSelect.value) || 0;
300
+ drawPreview();
301
+ });
302
+ modeBox.addEventListener("change", () => {
303
+ try {
304
+ setTemplate(editor, modeBox.checked);
305
+ say(modeBox.checked ? "Marked as a template." : "Marked as a normal presentation.");
306
+ } catch (error) {
307
+ modeBox.checked = isTemplateDocument(editor.document);
308
+ say(messageOf(error), true);
309
+ }
310
+ });
311
+
312
+ function insertTarget() {
313
+ const target = getTarget?.();
314
+ if (!target?.path) {
315
+ say("Select a text field first.", true);
316
+ return undefined;
317
+ }
318
+ return target;
319
+ }
320
+ insertButton.addEventListener("click", () => {
321
+ const target = insertTarget();
322
+ if (!target || !insertSelect.value) return;
323
+ try {
324
+ const result = insertVariableToken(editor, target.path, insertSelect.value, { start: target.start, end: target.end });
325
+ say(`Inserted ${result.token}.`);
326
+ } catch (error) {
327
+ say(messageOf(error), true);
328
+ }
329
+ });
330
+ newButton.addEventListener("click", () => {
331
+ const target = insertTarget();
332
+ if (!target) return;
333
+ const name = newName.value.trim();
334
+ if (!name) {
335
+ say("Give the new variable a name.", true);
336
+ return;
337
+ }
338
+ const variableId = suggestVariableId(editor.document, name);
339
+ const kind = newKind.value;
340
+ const declaration = { type: kind, label: name };
341
+ const sample = newSample.value.trim();
342
+ if (sample) {
343
+ // A template keeps the sample as an example (the slot stays unfilled); a normal deck needs a real value.
344
+ const field = isTemplateDocument(editor.document) ? "example" : "value";
345
+ declaration[field] = kind === "number" && Number.isFinite(Number(sample)) ? Number(sample) : kind === "list" ? sample.split(/\r?\n|,/).map((entry) => entry.trim()).filter(Boolean) : sample;
346
+ }
347
+ try {
348
+ const result = insertVariableToken(editor, target.path, variableId, { start: target.start, end: target.end, declare: declaration });
349
+ newName.value = "";
350
+ newSample.value = "";
351
+ say(`Created ${variableId} and inserted ${result.token}.`);
352
+ } catch (error) {
353
+ say(messageOf(error), true);
354
+ }
355
+ });
356
+
357
+ const unsubscribeEditor = editor.subscribe(() => rebuild());
358
+ const unsubscribeFill = fill.subscribe(() => update());
359
+ rebuild();
360
+
361
+ return {
362
+ element: root,
363
+ fill,
364
+ /** Re-read the session (after the host changed the document or the slide). */
365
+ refresh() {
366
+ previewSlide = options.getSlideIndex?.() ?? previewSlide;
367
+ rebuild();
368
+ },
369
+ destroy() {
370
+ destroyed = true;
371
+ unsubscribeEditor();
372
+ unsubscribeFill();
373
+ root.remove();
374
+ },
375
+ };
376
+ }
377
+
@@ -0,0 +1,126 @@
1
+ import type { EditorSession } from "./index.js";
2
+
3
+ export type TemplateVariableKind = "color" | "text" | "number" | "date" | "image" | "url" | "list";
4
+ export type TemplateFieldStatus = "filled" | "default" | "unfilled" | "optional";
5
+
6
+ /** The form control each variable kind uses. */
7
+ export declare const TEMPLATE_INPUT_TYPES: Readonly<Record<TemplateVariableKind, TemplateVariableKind>>;
8
+
9
+ export interface TemplateVariableUse {
10
+ id: string;
11
+ /** JSON pointer of the string carrying the token or reference. */
12
+ path: string;
13
+ form: "token" | "reference";
14
+ }
15
+
16
+ export interface TemplateField {
17
+ id: string;
18
+ kind: TemplateVariableKind;
19
+ input: TemplateVariableKind;
20
+ /** The declaration's `label`, or the id as words. */
21
+ label: string;
22
+ description?: string;
23
+ required: boolean;
24
+ format?: string;
25
+ example?: unknown;
26
+ /** The effective value: a supplied value, else the declared one. */
27
+ value?: unknown;
28
+ /** The declaration's own value (its default), if any. */
29
+ defaultValue?: unknown;
30
+ /** What a form control shows for a supplied value (empty when only the default applies). */
31
+ text: string;
32
+ /** Placeholder text: the default value, else the example. */
33
+ placeholder: string;
34
+ /** True when the value is rich text (formatting is kept until the field is edited). */
35
+ rich: boolean;
36
+ /** "filled" (supplied), "default" (declared value), "unfilled" (required, no value) or "optional". */
37
+ status: TemplateFieldStatus;
38
+ uses: TemplateVariableUse[];
39
+ }
40
+
41
+ export interface TemplateStatus {
42
+ template: boolean;
43
+ fieldCount: number;
44
+ requiredCount: number;
45
+ filledRequiredCount: number;
46
+ unfilled: string[];
47
+ complete: boolean;
48
+ }
49
+
50
+ export interface TemplatePreview {
51
+ presentation: Record<string, unknown>;
52
+ diagnostics: { code: string; severity: "error" | "warning" | "info"; path: string; id: string; message: string }[];
53
+ unfilled: string[];
54
+ examplesUsed: string[];
55
+ complete: boolean;
56
+ }
57
+
58
+ /** True when the core this editor runs on can resolve template variables. */
59
+ export declare function templatesAvailable(): boolean;
60
+ export declare function isTemplateDocument(document: unknown): boolean;
61
+ /** True when the document is a template or declares a variable that is not a color. */
62
+ export declare function hasTemplateVariables(document: unknown): boolean;
63
+ export declare function valueToFieldText(kind: TemplateVariableKind, value: unknown): string;
64
+ /** Parse a form control's text. A blank control clears the value. */
65
+ export declare function fieldTextToValue(kind: TemplateVariableKind, text: string): { ok: true; value: unknown } | { ok: false; message: string };
66
+ export declare function listTemplateFields(document: unknown, values?: Record<string, unknown>): TemplateField[];
67
+ export declare function templateStatus(document: unknown, values?: Record<string, unknown>): TemplateStatus;
68
+ /** The concrete deck for the values with each unfilled variable's example. Never throws for missing values. */
69
+ export declare function previewTemplate(document: unknown, values?: Record<string, unknown>): TemplatePreview;
70
+
71
+ export interface TemplateFillOptions {
72
+ /** Extra session meta for the fill edit. */
73
+ meta?: Record<string, unknown>;
74
+ }
75
+ export interface TemplateFillApplyResult {
76
+ document: unknown;
77
+ patches: unknown[];
78
+ inversePatches: unknown[];
79
+ validation: unknown;
80
+ unfilled: string[];
81
+ complete: boolean;
82
+ diagnostics: TemplatePreview["diagnostics"];
83
+ }
84
+ export interface TemplateFill {
85
+ readonly values: Record<string, unknown>;
86
+ fields(): TemplateField[];
87
+ status(): TemplateStatus;
88
+ preview(): TemplatePreview;
89
+ /** Set a typed value; `undefined` or `null` clears it. Throws on an unknown variable or a value of the wrong kind. */
90
+ set(id: string, value: unknown): TemplateFill;
91
+ /** Set from a form control's text. Returns `{ ok: false, message }` instead of throwing for bad text. */
92
+ setText(id: string, text: string): { ok: true; value: unknown } | { ok: false; message: string };
93
+ clear(id: string): TemplateFill;
94
+ /** Drop values for variables the document no longer declares. */
95
+ prune(): TemplateFill;
96
+ reset(): TemplateFill;
97
+ subscribe(listener: (fill: TemplateFill) => void): () => void;
98
+ /**
99
+ * Fill the document with the values as one undoable, validated edit. Unfilled required variables
100
+ * refuse the fill (`unfilled-variables`) unless `partial` is set.
101
+ */
102
+ apply(options?: { partial?: boolean; meta?: Record<string, unknown> }): TemplateFillApplyResult;
103
+ }
104
+ export declare function createTemplateFill(editor: EditorSession, options?: TemplateFillOptions): TemplateFill;
105
+
106
+ /** `{{id}}`, or `{{id|format}}`. */
107
+ export declare function variableToken(id: string, format?: string): string;
108
+ export declare function suggestVariableId(document: unknown, label?: string): string;
109
+ /** Declare a variable as one undoable edit. */
110
+ export declare function declareVariable(editor: EditorSession, id: string, declaration: string | Record<string, unknown>, meta?: Record<string, unknown>): unknown;
111
+ /** Mark the document as a template, or as a normal deck with `false`, as one undoable edit. */
112
+ export declare function setTemplate(editor: EditorSession, enabled: boolean, meta?: Record<string, unknown>): unknown;
113
+ export interface InsertVariableTokenOptions {
114
+ /** UTF-16 offsets into the string; a selection is replaced. Default: the end. */
115
+ start?: number;
116
+ end?: number;
117
+ /** For a TextRun[] field: which run (default the last). */
118
+ runIndex?: number;
119
+ /** A one-off format: `{{id|format}}`. */
120
+ format?: string;
121
+ /** Declare the variable in the same edit when it is not declared yet. */
122
+ declare?: string | Record<string, unknown>;
123
+ meta?: Record<string, unknown>;
124
+ }
125
+ /** Insert a variable token into a text field as one undoable edit. */
126
+ export declare function insertVariableToken(editor: EditorSession, path: string, id: string, options?: InsertVariableTokenOptions): { token: string; document: unknown; patches: unknown[]; inversePatches: unknown[]; validation: unknown };