@openpresentation/opf-editor 0.15.0 → 0.17.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/README.md +42 -14
- package/dist/canvas.js +14 -2
- package/dist/catalog-control.js +21 -0
- package/dist/design-controls.js +88 -27
- package/dist/design-options.d.ts +22 -9
- package/dist/design-options.js +77 -18
- package/dist/export.d.ts +6 -1
- package/dist/export.js +6 -1
- package/dist/find-replace.js +10 -1
- package/dist/font-gate.js +0 -11
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -9
- package/dist/json-options.js +7 -6
- package/dist/layout-handles.js +13 -0
- package/dist/layout-placeholders.js +15 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +5 -1
- package/dist/slide-manager.js +3 -1
- package/dist/svelte.d.ts +1 -0
- package/dist/svelte.js +3 -2
- package/dist/switches.js +3 -2
- package/dist/table-options.d.ts +6 -1
- package/dist/table-options.js +43 -0
- package/dist/template-panel.js +6 -4
- package/dist/templates.d.ts +3 -1
- package/dist/templates.js +1 -1
- package/package.json +18 -9
package/dist/design-options.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// session form that commits it with `meta.source: "design-option"`.
|
|
6
6
|
import { getValueAtPath, opfPathToJsonPointer } from "./index.js";
|
|
7
7
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
8
|
+
import { listBuiltinVariables } from "@openpresentation/opf";
|
|
8
9
|
import { checkFormat } from "./checks.js";
|
|
9
10
|
|
|
10
11
|
/** The `design.logo` variant slots of a LogoSet, in schema order. */
|
|
@@ -24,8 +25,30 @@ export const LOGO_VARIANTS = Object.freeze([
|
|
|
24
25
|
]);
|
|
25
26
|
export const HEADER_FOOTER_ZONES = Object.freeze(["left", "center", "right"]);
|
|
26
27
|
// Flag fields of a header/footer zone: false is stored as "absent" (so is a `date` of false).
|
|
27
|
-
const ZONE_FLAGS = ["logo", "
|
|
28
|
-
export const ZONE_FIELDS = Object.freeze(["logo", "text", "image", "
|
|
28
|
+
const ZONE_FLAGS = ["logo", "socials"];
|
|
29
|
+
export const ZONE_FIELDS = Object.freeze(["logo", "text", "image", "date", "dateFormat", "socials"]);
|
|
30
|
+
// FA-31: generated values are `{{ }}` variables inside a zone's `text`. The keys 0.16 had for them are gone; say what to write instead.
|
|
31
|
+
const REMOVED_ZONE_FIELDS = Object.freeze({
|
|
32
|
+
organization: "Write {{organization.name}} in the text.",
|
|
33
|
+
speaker: "Write {{speaker.name}} in the text.",
|
|
34
|
+
section: "Write {{slide.section}} in the text.",
|
|
35
|
+
slideNumber: "Write {{slide.number}} in the text.",
|
|
36
|
+
slideNumberFormat: "Write {{slide.number}} and {{deck.slideCount}} in the text.",
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* The values the "Insert value" menu of a header or footer zone offers, in menu order. `token` is what lands in the zone's `text`.
|
|
40
|
+
* `{{slide.number}}`, `{{deck.slideCount}}` and `{{slide.section}}` vary per slide; the others come from the deck's own fields.
|
|
41
|
+
*/
|
|
42
|
+
export const ZONE_VALUES = Object.freeze(
|
|
43
|
+
[
|
|
44
|
+
{ name: "slide.number", label: "Slide number" },
|
|
45
|
+
{ name: "deck.slideCount", label: "Slide count" },
|
|
46
|
+
{ name: "slide.section", label: "Section" },
|
|
47
|
+
{ name: "organization.name", label: "Organization" },
|
|
48
|
+
{ name: "speaker.name", label: "Speaker" },
|
|
49
|
+
{ name: "deck.name", label: "Deck name" },
|
|
50
|
+
].map((entry) => Object.freeze({ ...entry, token: `{{${entry.name}}}` })),
|
|
51
|
+
);
|
|
29
52
|
/** Date tokens a `dateFormat` understands (English names, independent of the host locale). */
|
|
30
53
|
export const DATE_FORMAT_TOKENS = Object.freeze(["yyyy", "yy", "MMMM", "MMM", "MM", "M", "dd", "d", "EEEE", "EEE"]);
|
|
31
54
|
const ISO_DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
@@ -36,7 +59,6 @@ function checkZoneField(key, value) {
|
|
|
36
59
|
if (ZONE_FLAGS.includes(key) && typeof value !== "boolean") throw bad(`${key} is true or false.`);
|
|
37
60
|
if (key === "text" && typeof value !== "string") throw bad("Text is a string.");
|
|
38
61
|
if (key === "image" && typeof value !== "string" && !isObject(value)) throw bad("An image is a source, an asset reference or an asset object.");
|
|
39
|
-
if (key === "slideNumberFormat" && (typeof value !== "string" || !value.includes("{current}"))) throw bad("The slide number format must contain {current}, for example Page {current} of {total}.");
|
|
40
62
|
if (key === "date" && typeof value !== "boolean" && typeof value !== "string") throw bad("A date is true (the current date) or a fixed date.");
|
|
41
63
|
if (key === "dateFormat" && (typeof value !== "string" || !value.trim())) throw bad(`A date format uses tokens such as ${DATE_FORMAT_TOKENS.join(", ")}.`);
|
|
42
64
|
}
|
|
@@ -90,20 +112,23 @@ function primaryOrganization(presentation) {
|
|
|
90
112
|
return list.find((entry) => entry?.role === "primary") ?? list[0];
|
|
91
113
|
}
|
|
92
114
|
|
|
93
|
-
function firstSpeaker(presentation) {
|
|
94
|
-
const list = Array.isArray(presentation.speaker) ? presentation.speaker : presentation.speaker ? [presentation.speaker] : [];
|
|
95
|
-
return list[0];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
115
|
/** Whether a logo resolves for the slide: slide design, then deck design, then the primary organization. */
|
|
99
116
|
export function hasResolvableLogo(presentation, slideIndex) {
|
|
100
117
|
return Boolean(presentation.slides?.[slideIndex]?.design?.logo ?? presentation.design?.logo ?? primaryOrganization(presentation)?.logo);
|
|
101
118
|
}
|
|
102
119
|
|
|
120
|
+
/** The variable names a zone's `text` uses (`{{name}}` and `{{name|format}}`); a token with a backslash before it is literal text. */
|
|
121
|
+
function zoneTextVariables(text) {
|
|
122
|
+
const names = [];
|
|
123
|
+
for (const match of text.matchAll(/(\\?)\{\{\s*([A-Za-z0-9_.-]+)\s*(?:\|[^{}]*)?\}\}/g)) if (!match[1]) names.push(match[2]);
|
|
124
|
+
return names;
|
|
125
|
+
}
|
|
126
|
+
|
|
103
127
|
/**
|
|
104
|
-
* Plain-language warnings for settings that need
|
|
105
|
-
*
|
|
106
|
-
*
|
|
128
|
+
* Plain-language warnings for settings that need content the document does not have: a logo (a header or footer
|
|
129
|
+
* zone with `logo: true`, picture bullets), the organization's social profiles, or a value a zone's `text` asks for
|
|
130
|
+
* with a variable (`{{organization.name}}` without an organization, `{{slide.section}}` on a slide without a section).
|
|
131
|
+
* The renderers fall back to a glyph or report unresolved content; the editor surfaces it before export.
|
|
107
132
|
*/
|
|
108
133
|
export function designWarnings(presentation, slideIndex = 0) {
|
|
109
134
|
const slide = presentation.slides?.[slideIndex];
|
|
@@ -115,15 +140,25 @@ export function designWarnings(presentation, slideIndex = 0) {
|
|
|
115
140
|
if (!hasLogo && isObject(design[which]) && design[which][zone]?.logo === true)
|
|
116
141
|
warnings.push({ code: "unresolved-logo", path: `design.${which}.${zone}.logo`, message: `The ${which} ${zone} zone shows the logo, but no logo is set. Add a logo or an organization logo.` });
|
|
117
142
|
const organization = primaryOrganization(presentation);
|
|
143
|
+
let builtins;
|
|
118
144
|
for (const which of ["header", "footer"])
|
|
119
145
|
for (const zone of HEADER_FOOTER_ZONES) {
|
|
120
146
|
const item = isObject(design[which]) ? design[which][zone] : undefined;
|
|
121
|
-
if (item?.organization === true && !organization)
|
|
122
|
-
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.organization`, message: `The ${which} ${zone} zone shows the organization, but the presentation has none.` });
|
|
123
|
-
if (item?.speaker === true && !firstSpeaker(presentation)?.name)
|
|
124
|
-
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.speaker`, message: `The ${which} ${zone} zone shows the speaker, but the presentation has no named speaker.` });
|
|
125
147
|
if (item?.socials === true && !organization?.socials)
|
|
126
148
|
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.socials`, message: `The ${which} ${zone} zone shows social profiles, but the organization has none.` });
|
|
149
|
+
// FA-31: a value the zone's text asks for and the document cannot give (the core validator reports the same as variable-builtin-missing).
|
|
150
|
+
const path = `design.${which}.${zone}.text`;
|
|
151
|
+
for (const name of new Set(typeof item?.text === "string" ? zoneTextVariables(item.text) : [])) {
|
|
152
|
+
if (name === "slide.section") {
|
|
153
|
+
if (!(typeof slide?.section === "string" && slide.section !== ""))
|
|
154
|
+
warnings.push({ code: "unresolved-content", path, message: `The ${which} ${zone} zone shows {{slide.section}}, but this slide has no section.` });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
builtins ??= listBuiltinVariables(presentation);
|
|
158
|
+
const entry = builtins.find((candidate) => candidate.name === name);
|
|
159
|
+
if (entry?.scope === "deck" && !entry.available)
|
|
160
|
+
warnings.push({ code: "unresolved-content", path, message: `The ${which} ${zone} zone shows {{${name}}}, but the presentation has no value for it.` });
|
|
161
|
+
}
|
|
127
162
|
}
|
|
128
163
|
if (!hasLogo && design.listBullet === "image")
|
|
129
164
|
warnings.push({ code: "unresolved-logo", path: "design.listBullet", message: "Picture bullets use the logo, but no logo is set, so lists draw the character bullet. Add a logo or an organization logo." });
|
|
@@ -343,8 +378,9 @@ export function readLogoVariants(presentation, options = {}) {
|
|
|
343
378
|
|
|
344
379
|
/**
|
|
345
380
|
* Compute the patch that edits one header or footer zone. `fields` merges into the zone (logo, text,
|
|
346
|
-
* image,
|
|
347
|
-
*
|
|
381
|
+
* image, date, dateFormat, socials); `null`, `false` for a flag, or an empty string removes a field.
|
|
382
|
+
* Generated values (slide number, slide count, section, organization, speaker, deck name) are `{{ }}`
|
|
383
|
+
* variables in `text` (`ZONE_VALUES`, `insertZoneValue`). A zone left empty is removed, then an empty
|
|
348
384
|
* header or footer, so a slide never carries `{}`. A slide's own header or footer replaces the deck's
|
|
349
385
|
* whole one, so the first edit on a slide that has none of its own starts from a copy of the deck's
|
|
350
386
|
* (the other zones stay); a slide emptied that way hides the furniture (`false`) instead of
|
|
@@ -356,7 +392,7 @@ export function prepareHeaderFooterZone(presentation, which, zone, fields, optio
|
|
|
356
392
|
if (!HEADER_FOOTER_ZONES.includes(zone)) throw fail("invalid-design-value", `Zone is one of ${HEADER_FOOTER_ZONES.join(", ")}.`, { zone });
|
|
357
393
|
if (!isObject(fields)) throw fail("invalid-design-value", "Pass the zone fields to change.", { fields });
|
|
358
394
|
const unknown = Object.keys(fields).filter((key) => !ZONE_FIELDS.includes(key));
|
|
359
|
-
if (unknown.length) throw fail("invalid-design-value", `Unknown header/footer field: ${unknown[0]}
|
|
395
|
+
if (unknown.length) throw fail("invalid-design-value", `Unknown header/footer field: ${unknown[0]}.${Object.hasOwn(REMOVED_ZONE_FIELDS, unknown[0]) ? ` ${REMOVED_ZONE_FIELDS[unknown[0]]}` : ""}`, { fields });
|
|
360
396
|
const { scope, base, slideIndex } = scopeOf(presentation, "logo", options);
|
|
361
397
|
const own = ownDesign(presentation, base)[which];
|
|
362
398
|
const inherited = base.length ? presentation.design?.[which] : undefined;
|
|
@@ -387,6 +423,29 @@ export function setHeaderFooterZone(editor, which, zone, fields, options = {}) {
|
|
|
387
423
|
const { meta, ...rest } = options;
|
|
388
424
|
return apply(editor, prepareHeaderFooterZone(editor.presentation, which, zone, fields, rest), meta);
|
|
389
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Compute the patch that inserts a value's token (`ZONE_VALUES`, for example `slide.number` for `{{slide.number}}`) into a zone's
|
|
428
|
+
* `text` at UTF-16 offsets `start` and `end` (a selection is replaced; default: the end of the text). The text is the one the scope
|
|
429
|
+
* shows (the slide's own zone, else the deck's), or `options.text` when a panel holds edits it has not committed yet.
|
|
430
|
+
*/
|
|
431
|
+
export function prepareZoneValue(presentation, which, zone, name, options = {}) {
|
|
432
|
+
const value = ZONE_VALUES.find((entry) => entry.name === name);
|
|
433
|
+
if (!value) throw fail("invalid-design-value", `Insert one of ${ZONE_VALUES.map((entry) => entry.name).join(", ")}.`, { value: name });
|
|
434
|
+
const { start, end, text: typed, ...rest } = options;
|
|
435
|
+
if (typed !== undefined && typeof typed !== "string") throw fail("invalid-design-value", "Text is a string.", { text: typed });
|
|
436
|
+
const current = typed ?? readHeaderFooterZone(presentation, which, zone, rest).text;
|
|
437
|
+
const text = typeof current === "string" ? current : "";
|
|
438
|
+
const from = start === undefined ? text.length : start;
|
|
439
|
+
const to = end === undefined ? from : end;
|
|
440
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from || to > text.length) throw fail("invalid-selection", "The selection is outside the text.", { start: from, end: to });
|
|
441
|
+
return prepareHeaderFooterZone(presentation, which, zone, { text: `${text.slice(0, from)}${value.token}${text.slice(to)}` }, rest);
|
|
442
|
+
}
|
|
443
|
+
/** Insert a value's token into one zone's `text` as a single undoable transaction. */
|
|
444
|
+
export function insertZoneValue(editor, which, zone, name, options = {}) {
|
|
445
|
+
checkEditor(editor);
|
|
446
|
+
const { meta, ...rest } = options;
|
|
447
|
+
return apply(editor, prepareZoneValue(editor.presentation, which, zone, name, rest), meta);
|
|
448
|
+
}
|
|
390
449
|
/**
|
|
391
450
|
* One header or footer zone's fields as they apply at a scope: the slide's own header (or footer) when it
|
|
392
451
|
* has one, else the deck's (`{}` when absent or suppressed). `headerFooterState` says which.
|
package/dist/export.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export interface ExportOptions {
|
|
|
42
42
|
includeHidden?: boolean;
|
|
43
43
|
/** "vector" (default): selectable text and vector shapes; "raster": an image per slide. */
|
|
44
44
|
pdfMode?: "vector" | "raster";
|
|
45
|
+
/**
|
|
46
|
+
* Raster PDF only: the pdf-lib module (`import * as pdfLib from "pdf-lib"`). The renderer's `export-browser` entry imports no PDF library (opf-render 0.16), so the host passes it;
|
|
47
|
+
* without it a `pdfMode: "raster"` PDF rejects with the renderer's `converter-missing`. Vector PDF, PNG and SVG need no option.
|
|
48
|
+
*/
|
|
49
|
+
pdfLib?: { PDFDocument: { create(options?: { updateMetadata?: boolean }): Promise<any> } };
|
|
45
50
|
/** PNG pixel density (and the raster PDF's), 1 to 4; default 2. */
|
|
46
51
|
scale?: number;
|
|
47
52
|
/** The options the host draws its preview with (`catalogs`, `date`, ...); the fonts go in `fonts`. */
|
|
@@ -72,6 +77,6 @@ export interface ExportResult {
|
|
|
72
77
|
}
|
|
73
78
|
/**
|
|
74
79
|
* Draw the deck with the renderer the preview uses and convert it. Rejects with an error whose `code` is `export-aborted`, `export-fonts-unlicensed` (the license rule left the PDF converter no face for some text), `export-no-slides`,
|
|
75
|
-
* `export-format`, `export-unavailable` (PDF and PNG need the renderer's `export-browser` entry), `fonts-unavailable` or a renderer code.
|
|
80
|
+
* `export-format`, `export-unavailable` (PDF and PNG need the renderer's `export-browser` entry), `fonts-unavailable` or a renderer code (`converter-missing` for a raster PDF without `pdfLib`).
|
|
76
81
|
*/
|
|
77
82
|
export declare function exportDeck(deck: unknown, options: ExportOptions): Promise<ExportResult>;
|
package/dist/export.js
CHANGED
|
@@ -124,6 +124,9 @@ async function loadConverters(options) {
|
|
|
124
124
|
* - `format`: "pdf", "png" or "svg" (required).
|
|
125
125
|
* - `slides`: "current" (with `slideIndex`), "all" (default; hidden slides only with `includeHidden`) or an array of slide numbers.
|
|
126
126
|
* - `pdfMode`: "vector" (default: selectable text, vector shapes) or "raster" (each slide an image).
|
|
127
|
+
* - `pdfLib`: for `pdfMode: "raster"` only, the pdf-lib module (`import * as pdfLib from "pdf-lib"`). The renderer's `export-browser` entry imports no PDF library (RR-63, opf-render 0.16),
|
|
128
|
+
* so a host that offers the raster PDF imports pdf-lib itself; without it a raster PDF rejects with the renderer's `converter-missing` (its `details.install` names the package).
|
|
129
|
+
* Vector PDF, PNG and SVG need no option.
|
|
127
130
|
* - `scale`: PNG pixel density, 1 to 4 (default 2); also the raster PDF's density.
|
|
128
131
|
* - `renderOptions`: the render options the host draws with (`catalogs`, `date`, ...), the same as its preview.
|
|
129
132
|
* - `catalogs`: the host's registered catalogs (`Catalog[]`, merged with `renderOptions.catalogs`). The deck is embedded first (core's
|
|
@@ -137,7 +140,7 @@ async function loadConverters(options) {
|
|
|
137
140
|
* `export-fonts-unlicensed` (the converter's error is its `cause`).
|
|
138
141
|
* - `signal`, `onProgress({ stage, done, total, message })`, `onDiagnostic(diagnostic)`.
|
|
139
142
|
* Resolves `{ download, files, diagnostics, slides }`. Rejects with an error whose `code` is `export-aborted`,
|
|
140
|
-
* `export-fonts-unlicensed`, `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code.
|
|
143
|
+
* `export-fonts-unlicensed`, `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code (`converter-missing` for a raster PDF without `pdfLib`).
|
|
141
144
|
*/
|
|
142
145
|
export async function exportDeck(input, options = {}) {
|
|
143
146
|
const format = EXPORT_FORMATS[options.format];
|
|
@@ -213,6 +216,8 @@ export async function exportDeck(input, options = {}) {
|
|
|
213
216
|
metadata,
|
|
214
217
|
signal,
|
|
215
218
|
fontData: pdfFontData(embeddedFonts),
|
|
219
|
+
// RR-63: the renderer's browser entry imports no pdf-lib; raster mode takes the module the host imported.
|
|
220
|
+
...(options.pdfMode === "raster" && options.pdfLib ? { pdfLib: options.pdfLib } : {}),
|
|
216
221
|
...(options.fallbackFamily ? { defaultFontFamily: options.fallbackFamily } : {}),
|
|
217
222
|
onDiagnostic: (diagnostic) => { const described = describeDiagnostic(diagnostic, "pdf"); if (keep(described)) note(described); },
|
|
218
223
|
onProgress: ({ page, pages }) => progress("convert", page, pages, `Page ${page} of ${pages}`),
|
package/dist/find-replace.js
CHANGED
|
@@ -21,6 +21,11 @@ const REGION_KEY = /^(?:top|middle|bottom|left|center|right)(?:[+:]|$)/;
|
|
|
21
21
|
const WORD_BEFORE = "(?<![\\p{L}\\p{N}_])";
|
|
22
22
|
const WORD_AFTER = "(?![\\p{L}\\p{N}_])";
|
|
23
23
|
|
|
24
|
+
// A variable token (`{{customer}}`, `{{slide.number}}`, `{{ deck.slideCount }}`, `{{revenue|0.0}}`). It is an instruction, not text: a match inside it is
|
|
25
|
+
// never offered, because replacing part of a name would either break the variable or silently change which value it shows (FA-31). An
|
|
26
|
+
// escaped `\{{` is literal text and is searched like the rest.
|
|
27
|
+
const VARIABLE_TOKEN = /(?<!\\)\{\{\s*[A-Za-z][A-Za-z0-9_.-]*\s*(?:\|[^{}]*)?\}\}/g;
|
|
28
|
+
|
|
24
29
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25
30
|
const runText = (run) => (typeof run === "string" ? run : isObject(run) && typeof run.text === "string" ? run.text : "");
|
|
26
31
|
const isRuns = (value) => Array.isArray(value) && value.length > 0 && value.every((run) => typeof run === "string" || (isObject(run) && typeof run.text === "string"));
|
|
@@ -107,6 +112,7 @@ function payloadFields(fields, state, base, payload, prefix = "") {
|
|
|
107
112
|
}));
|
|
108
113
|
}
|
|
109
114
|
if (isObject(payload.chart)) field(fields, state, at("chart", "alt"), payload.chart.alt, `${prefix}Chart alt text`, "alt");
|
|
115
|
+
if (isObject(payload.table)) field(fields, state, at("table", "alt"), payload.table.alt, `${prefix}Table alt text`, "alt");
|
|
110
116
|
const table = payload.table;
|
|
111
117
|
if (isObject(table)) {
|
|
112
118
|
if (Array.isArray(table.columns)) table.columns.forEach((cell, c) => (isObject(cell) && !("value" in cell) ? field(fields, state, at("table", "columns", String(c), "name"), cell.name, `${prefix}Table header ${c + 1}`, "table") : cellFields(fields, state, at("table", "columns", String(c)), cell, `${prefix}Table header ${c + 1}`)));
|
|
@@ -204,7 +210,8 @@ function snippet(text, start, end) {
|
|
|
204
210
|
* Search a document. Returns `{ matches, fieldCount, slideCount, truncated, error }`. A match is `{ id, pointer, path,
|
|
205
211
|
* slideIndex, label, runs, start, end, text, groups, named, context: { before, match, after } }` where `start` and `end`
|
|
206
212
|
* are offsets into the field's text. Options: those of `compileSearch`, plus `notes` and `slideIndex` of
|
|
207
|
-
* `collectSearchFields`. Matches that are empty (a regular expression that matches nothing) are skipped
|
|
213
|
+
* `collectSearchFields`. Matches that are empty (a regular expression that matches nothing) are skipped, and so is a match that
|
|
214
|
+
* touches a variable token (`{{slide.number}}`): the token is not text, and replacing part of it would break or change the variable.
|
|
208
215
|
*/
|
|
209
216
|
export function findMatches(presentation, query, options = {}) {
|
|
210
217
|
const compiled = compileSearch(query, options);
|
|
@@ -216,11 +223,13 @@ export function findMatches(presentation, query, options = {}) {
|
|
|
216
223
|
compiled.regex.lastIndex = 0;
|
|
217
224
|
let found;
|
|
218
225
|
let hit = false;
|
|
226
|
+
const tokens = item.text.includes("{{") ? [...item.text.matchAll(VARIABLE_TOKEN)].map((token) => [token.index, token.index + token[0].length]) : [];
|
|
219
227
|
while ((found = compiled.regex.exec(item.text))) {
|
|
220
228
|
if (found[0] === "") {
|
|
221
229
|
compiled.regex.lastIndex += 1;
|
|
222
230
|
continue;
|
|
223
231
|
}
|
|
232
|
+
if (tokens.some(([from, to]) => found.index < to && found.index + found[0].length > from)) continue;
|
|
224
233
|
if (result.matches.length >= MAX_MATCHES) {
|
|
225
234
|
result.truncated = true;
|
|
226
235
|
break outer;
|
package/dist/font-gate.js
CHANGED
|
@@ -13,17 +13,6 @@ export const FONTS_UNAVAILABLE = "fonts-unavailable";
|
|
|
13
13
|
|
|
14
14
|
const GATE = Symbol("opf-editor.fontGate");
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* The fonts a preview draws with: the handle's text measurement and nothing else. A full handle also lists `embeddedFonts`, which
|
|
18
|
-
* the renderer writes into every SVG it returns (an export needs that, so the SVG carries its faces); a preview inside a page
|
|
19
|
-
* that already loaded the faces would repeat megabytes of base64 in every canvas, thumbnail and panel preview. The measurement
|
|
20
|
-
* is read from the handle at each use, so faces it loads later still measure.
|
|
21
|
-
*/
|
|
22
|
-
export function previewFonts(fonts) {
|
|
23
|
-
if (!fonts || typeof fonts !== "object") return undefined;
|
|
24
|
-
return { get textMeasurement() { return fonts.textMeasurement; } };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
16
|
/**
|
|
28
17
|
* The gate over a fonts handle: the two calls the canvas and the export need, with the editor's error contract.
|
|
29
18
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -72,6 +72,7 @@ export interface EditorHistory { undo: EditorHistoryEntry[]; redo: EditorHistory
|
|
|
72
72
|
|
|
73
73
|
export interface EditorSession {
|
|
74
74
|
paginateSlide(slideIndex: number, options?: PaginationOptions & EditorDiagnosticOptions, meta?: Record<string, unknown>): { change: EditorChange | null; pagination: PaginationResult };
|
|
75
|
+
/** Composes the slide with `{{slide.number}}`, `{{slide.section}}` and `{{deck.slideCount}}` substituted for `options.slideNumber` and `options.slideCount` (default: its place in the open deck and the deck's slide count); the document keeps the tokens. */
|
|
75
76
|
composeSlide(slideIndex: number, options?: ComposeSlideOptions & EditorDiagnosticOptions): SlideComposition;
|
|
76
77
|
setComposition(slideIndex: number, composition: Composition, meta?: Record<string, unknown>): EditorChange;
|
|
77
78
|
readonly presentation: unknown;
|
|
@@ -162,6 +163,7 @@ export interface CatalogSelectOptions extends CatalogOptionsInput {
|
|
|
162
163
|
path: string;
|
|
163
164
|
catalogKind: string;
|
|
164
165
|
label?: string;
|
|
166
|
+
onError?: (error: Error) => void;
|
|
165
167
|
document?: { createElement(tagName: string): any };
|
|
166
168
|
}
|
|
167
169
|
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commitCatalogControl } from "./catalog-control.js";
|
|
1
2
|
import { paginateSlide } from "@openpresentation/opf/pagination";
|
|
2
3
|
import { OPFPatchError, applyPatch as applyCorePatch, applyPatchWithInverse, formatPointer, invertPatch, jsonEqual, parsePointer, readPointer } from "@openpresentation/opf/patch";
|
|
3
4
|
import { collectReservedPresentationIds } from "./presentation-ids.js";
|
|
@@ -31,12 +32,14 @@ export {
|
|
|
31
32
|
// theme, engine default), with the session's registered catalogs, so the editor measures and composes what the renderer draws
|
|
32
33
|
// and the exporter writes. `options.fonts` is the renderer's fonts handle (its `textMeasurement` measures); `options.catalogs`
|
|
33
34
|
// adds catalogs for this call; `options.onDiagnostic` hears each diagnostic (`unresolved-reference`);
|
|
34
|
-
// any other option overrides the resolved one (`layout`, ...).
|
|
35
|
+
// any other option overrides the resolved one (`layout`, ...). FA-31: `slide` is the slide to compose and draw, with `{{slide.number}}`,
|
|
36
|
+
// `{{slide.section}}` and `{{deck.slideCount}}` substituted for `slideNumber` and `slideCount` (default: its place in the open deck and the
|
|
37
|
+
// deck's length), the same numbers the options carry. Pagination takes the source slide instead and substitutes while it measures.
|
|
35
38
|
function slideContext(presentation, slideIndex, catalogs, { fonts, catalogs: _extra, onDiagnostic, ...overrides } = {}) {
|
|
36
39
|
if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
|
|
37
|
-
const { options, diagnostics } = resolveSlideContext(presentation, slideIndex, { fonts, catalogs });
|
|
40
|
+
const { slide, options, diagnostics } = resolveSlideContext(presentation, slideIndex, { fonts, catalogs, slideNumber: overrides.slideNumber, slideCount: overrides.slideCount });
|
|
38
41
|
for (const diagnostic of diagnostics) onDiagnostic?.(diagnostic);
|
|
39
|
-
return { ...options, ...overrides };
|
|
42
|
+
return { slide, options: { ...options, ...overrides } };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export const packageName = "@openpresentation/opf-editor";
|
|
@@ -275,12 +278,13 @@ export function createEditorSession(input, options = {}) {
|
|
|
275
278
|
});
|
|
276
279
|
},
|
|
277
280
|
composeSlide(slideIndex, options = {}) {
|
|
278
|
-
|
|
281
|
+
const { slide, options: resolved } = slideContext(presentation, slideIndex, mergeCatalogs(catalogs, options.catalogs), options);
|
|
282
|
+
return composeSlide(slide, resolved);
|
|
279
283
|
},
|
|
280
284
|
paginateSlide(slideIndex, options = {}, meta = {}) {
|
|
281
285
|
const list = mergeCatalogs(catalogs, options.catalogs);
|
|
282
286
|
// Core pagination reads the measurement from `fonts`, so the resolved context hands it over that way.
|
|
283
|
-
const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, list, options);
|
|
287
|
+
const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, list, options).options;
|
|
284
288
|
const pagination = paginateSlide(presentation.slides[slideIndex], {
|
|
285
289
|
...resolved,
|
|
286
290
|
catalogs: list,
|
|
@@ -523,8 +527,19 @@ export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
|
|
|
523
527
|
const current = editor.get(path);
|
|
524
528
|
const value = catalogReferenceValue(current, catalogId);
|
|
525
529
|
const { catalogs: _catalogs, presentation: _presentation, ...commitMeta } = meta;
|
|
526
|
-
|
|
530
|
+
// Only documented design references may initialize their optional design parent.
|
|
531
|
+
// Ordinary set/applyPatch retain RFC 6902's existing-parent requirement.
|
|
532
|
+
const segments = splitOpfPath(path);
|
|
533
|
+
const designField = { themes: "theme", colorSchemes: "colorScheme", fontSchemes: "fontScheme" }[catalogKind];
|
|
534
|
+
const deckDesign = segments.length === 2 && segments[0] === "design";
|
|
535
|
+
const slideDesign = segments.length === 4 && segments[0] === "slides" && /^(0|[1-9][0-9]*)$/.test(segments[1]) && segments[2] === "design" && editor.get(segments.slice(0, 2));
|
|
536
|
+
const parent = segments.slice(0, -1);
|
|
537
|
+
const patches = designField === segments.at(-1) && (deckDesign || slideDesign) && !hasValueAtPath(editor.presentation, parent)
|
|
538
|
+
? [{ op: "add", path: opfPathToJsonPointer(parent), value: { [designField]: value } }]
|
|
539
|
+
: createValuePatch(editor.presentation, path, value);
|
|
540
|
+
return editor.applyPatch(patches, {
|
|
527
541
|
...commitMeta,
|
|
542
|
+
rejectInvalid: true,
|
|
528
543
|
catalogKind,
|
|
529
544
|
catalogId,
|
|
530
545
|
source: commitMeta.source ?? "catalog-control"
|
|
@@ -589,13 +604,13 @@ export function createCatalogSelect(editor, options) {
|
|
|
589
604
|
const current = editor.get(path);
|
|
590
605
|
select.value = typeof current === "object" && current ? current.id : current ?? "";
|
|
591
606
|
};
|
|
592
|
-
const onChange = () => {
|
|
593
|
-
setCatalogId(editor, path, catalogKind, select.value, {
|
|
607
|
+
const onChange = () => commitCatalogControl(select, options?.onError, updateValue, () => {
|
|
608
|
+
return setCatalogId(editor, path, catalogKind, select.value, {
|
|
594
609
|
catalogs: options?.catalogs,
|
|
595
610
|
presentation: options?.presentation,
|
|
596
611
|
source: "catalog-select"
|
|
597
612
|
});
|
|
598
|
-
};
|
|
613
|
+
});
|
|
599
614
|
const unsubscribe = editor.subscribe(updateValue);
|
|
600
615
|
|
|
601
616
|
select.addEventListener?.("change", onChange);
|
package/dist/json-options.js
CHANGED
|
@@ -2,7 +2,7 @@ import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
|
2
2
|
import { listCatalogRecords } from "./catalogs.js";
|
|
3
3
|
import { schemaAtPath, schemaVariants } from "./schema.js";
|
|
4
4
|
import { applyEdits, findNodeAtLocation, findNodeAtOffset, getNodePath, modify, parseTree } from "jsonc-parser";
|
|
5
|
-
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
5
|
+
import { layoutSlotSummary, layoutSlotTypes, populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
6
6
|
// The catalog kind a field references (OPF 0.15: every content kind is `id` or `name:id`), read from where the field sits.
|
|
7
7
|
const ROOT_REFERENCES = { narrative: "narratives", audience: "audiences", purpose: "purposes", tone: "tones" };
|
|
8
8
|
const DESIGN_REFERENCES = { theme: "themes", colorScheme: "colorSchemes", fontScheme: "fontSchemes" };
|
|
@@ -21,10 +21,9 @@ function catalogAtPath(path) {
|
|
|
21
21
|
}
|
|
22
22
|
// Compare declared placeholders, including multiplicity. This describes a layout
|
|
23
23
|
// contract, not a claim that arbitrary slide content will render without overflow.
|
|
24
|
+
// FA-26: a record's placeholder groups compare by their leaf regions, the slots its content fills.
|
|
24
25
|
function layoutTypes(record) {
|
|
25
|
-
|
|
26
|
-
return undefined;
|
|
27
|
-
return record.placeholders.map(p => p.type).sort();
|
|
26
|
+
return layoutSlotTypes(record)?.sort();
|
|
28
27
|
}
|
|
29
28
|
function placeholderSummary(types) {
|
|
30
29
|
if (!types)
|
|
@@ -57,8 +56,10 @@ function describeOptions(choices, records, current, catalog) {
|
|
|
57
56
|
if (catalog !== "layouts")
|
|
58
57
|
continue;
|
|
59
58
|
const types = layoutTypes(option.value === current ? currentRecord : records.get(String(option.value)));
|
|
60
|
-
|
|
61
|
-
option.
|
|
59
|
+
const record = records.get(String(option.value));
|
|
60
|
+
option.placeholderTypes = layoutSlotTypes(record);
|
|
61
|
+
// A record with placeholder groups also shows how its slots nest.
|
|
62
|
+
option.placeholders = placeholderSummary(types) + (record?.placeholders?.some?.(entry => entry?.type === "group") ? ` · ${layoutSlotSummary(record)}` : "");
|
|
62
63
|
const same = types && currentTypes && signature(types) === signature(currentTypes);
|
|
63
64
|
const equivalent = types && currentTypes && signature(compatible(types)) === signature(compatible(currentTypes));
|
|
64
65
|
const sameKinds = types && currentTypes && signature([...new Set(compatible(types))]) === signature([...new Set(compatible(currentTypes))]);
|
package/dist/layout-handles.js
CHANGED
|
@@ -60,6 +60,19 @@ export function createLayoutHandles(root, options) {
|
|
|
60
60
|
lastGeometry=geometry;
|
|
61
61
|
layer.replaceChildren();
|
|
62
62
|
if(!enabled||options.isTextEditing())return;
|
|
63
|
+
// FA-26: the slots of a layout record with placeholder groups, drawn under the handles while arranging: each group and
|
|
64
|
+
// region as a dashed outline at its cell, deeper groups inset, an empty region labelled with its kind. They are the
|
|
65
|
+
// record's, not the document's, so they are not editable here; the content in a slot keeps its own selection.
|
|
66
|
+
for(const slot of geometry.slots??[]){
|
|
67
|
+
const node=doc.createElement('div');
|
|
68
|
+
node.dataset.layoutSlot=slot.path;node.dataset.layoutSlotType=slot.type;node.setAttribute('aria-hidden','true');
|
|
69
|
+
const group=slot.type==='group',empty=!group&&!slot.content;
|
|
70
|
+
node.title=group?`Layout group (${slot.path})`:`${empty?'Empty ':''}${slot.type} slot (${slot.path})`;
|
|
71
|
+
const inset=group?Math.min(4,slot.depth*2):0;
|
|
72
|
+
node.style.cssText=`position:absolute;pointer-events:none;box-sizing:border-box;left:calc(${slot.box.x/geometry.width*100}% + ${inset}px);top:calc(${slot.box.y/geometry.height*100}% + ${inset}px);width:calc(${slot.box.width/geometry.width*100}% - ${2*inset}px);height:calc(${slot.box.height/geometry.height*100}% - ${2*inset}px);border:1px dashed ${group?'#8170d5':'#8a78d7aa'};border-radius:4px;${empty?'background:#8a78d714;':''}`;
|
|
73
|
+
if(empty){const label=doc.createElement('span');label.textContent=`Empty ${slot.type} slot`;label.style.cssText='position:absolute;left:6px;top:4px;font:11px system-ui;color:#675394';node.append(label);}
|
|
74
|
+
layer.append(node);
|
|
75
|
+
}
|
|
63
76
|
for(const flow of geometry.flows??[]){
|
|
64
77
|
const {axis,size,tracks}=trackData(flow);
|
|
65
78
|
if(tracks.length<2||tracks.length>12||flow.itemCount!==flow.slotCount)continue;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import {layoutLeaves,layoutStructure} from '@openpresentation/opf/composition';
|
|
1
2
|
import {listBlockContainers,prepareBlockInsert} from './blocks.js';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The slot kinds a layout record declares, in reading order: its headings and the leaf regions of its placeholder groups
|
|
6
|
+
* (FA-26), the kinds a slide's flat content fills. Undefined when the record states no placeholders.
|
|
7
|
+
*/
|
|
8
|
+
export function layoutSlotTypes(record){
|
|
9
|
+
if(!Array.isArray(record?.placeholders))return undefined;
|
|
10
|
+
try{return layoutLeaves(record).map(leaf=>leaf.type);}catch{return record.placeholders.filter(entry=>entry?.type!=='group').map(entry=>entry?.type);}
|
|
11
|
+
}
|
|
12
|
+
/** A one-line description of a record's slots, nested groups in parentheses: `title, column (text, text), chart`. */
|
|
13
|
+
export function layoutSlotSummary(record){
|
|
14
|
+
if(!Array.isArray(record?.placeholders))return '';
|
|
15
|
+
try{return layoutStructure(record);}catch{return layoutSlotTypes(record).join(', ');}
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
const payloadFields=['text','items','bullets','image','video','chart','table','code','metric','quote','timeline'];
|
|
4
19
|
const headingFields=new Set(['title','subtitle','tag']);
|
|
5
20
|
const fields={text:'text',list:'items',image:'image',video:'video',chart:'chart',table:'table',code:'code',metric:'metric',quote:'quote',timeline:'timeline'};
|
package/dist/react.d.ts
CHANGED
package/dist/react.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commitCatalogControl } from "./catalog-control.js";
|
|
1
2
|
import {
|
|
2
3
|
getCatalogOptions,
|
|
3
4
|
getValueAtPath,
|
|
@@ -56,6 +57,7 @@ export function createOPFReactComponents(React) {
|
|
|
56
57
|
catalogKind,
|
|
57
58
|
label,
|
|
58
59
|
catalogOptions,
|
|
60
|
+
onError,
|
|
59
61
|
selectProps = {},
|
|
60
62
|
...props
|
|
61
63
|
}) {
|
|
@@ -78,7 +80,9 @@ export function createOPFReactComponents(React) {
|
|
|
78
80
|
selectProps.onChange?.(event);
|
|
79
81
|
props.onChange?.(event);
|
|
80
82
|
if (!event.defaultPrevented) {
|
|
81
|
-
|
|
83
|
+
const node = event.currentTarget;
|
|
84
|
+
commitCatalogControl(node, onError, () => { node.value = currentId ?? ""; }, () =>
|
|
85
|
+
setCatalogId(editor, path, catalogKind, node.value, { source: "react-catalog-select" }));
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
},
|
package/dist/slide-manager.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
addSection, addSlide, duplicateSlides, hasSections, listSections, moveSection, moveSlides, moveSlidesBy, removeSection, removeSlides, renameSection, setHidden, setSection, slideTitle,
|
|
8
8
|
} from "./slides.js";
|
|
9
9
|
import { listSwitchOptions } from "./switches.js";
|
|
10
|
+
import { layoutSlotSummary } from "./layout-placeholders.js";
|
|
10
11
|
|
|
11
12
|
let instances = 0;
|
|
12
13
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
@@ -503,7 +504,8 @@ export function createSlideManager(container, options) {
|
|
|
503
504
|
};
|
|
504
505
|
const describe = () => {
|
|
505
506
|
const choice = choices.find((entry) => entry.id === select.value);
|
|
506
|
-
|
|
507
|
+
// FA-26: nested placeholder groups show with their mode, for example `title, column (text, text), chart`.
|
|
508
|
+
const slots = layoutSlotSummary(choice?.record);
|
|
507
509
|
detail.textContent = choice ? `${choice.record?.description ?? ""}${slots ? ` Slots: ${slots}.` : choice.record?.placeholders ? " An empty slide." : ""}`.trim() : "No layout matches.";
|
|
508
510
|
};
|
|
509
511
|
const form = el("form", { method: "dialog" },
|
package/dist/svelte.d.ts
CHANGED
package/dist/svelte.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commitCatalogControl } from "./catalog-control.js";
|
|
1
2
|
import {
|
|
2
3
|
createSvgTraceBinding,
|
|
3
4
|
getCatalogOptions,
|
|
@@ -59,11 +60,11 @@ export function opfCatalogSelect(node, params) {
|
|
|
59
60
|
const value = editor.get(path);
|
|
60
61
|
node.value = value && typeof value === "object" ? value.id : value ?? "";
|
|
61
62
|
};
|
|
62
|
-
const onChange = () => setCatalogId(editor, path, catalogKind, node.value, {
|
|
63
|
+
const onChange = () => commitCatalogControl(node, nextParams.onError, sync, () => setCatalogId(editor, path, catalogKind, node.value, {
|
|
63
64
|
catalogs: nextParams.catalogs,
|
|
64
65
|
presentation: nextParams.presentation,
|
|
65
66
|
source: "svelte-catalog-select"
|
|
66
|
-
});
|
|
67
|
+
}));
|
|
67
68
|
const unsubscribe = editor.subscribe(sync);
|
|
68
69
|
|
|
69
70
|
node.addEventListener?.("change", onChange);
|
package/dist/switches.js
CHANGED
|
@@ -13,7 +13,7 @@ import { chartOptionTarget } from "@openpresentation/opf/composition";
|
|
|
13
13
|
import { createValuePatch, applyJsonPatch, getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
14
14
|
import { createContentBlock, prepareBlockReplace } from "./blocks.js";
|
|
15
15
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
16
|
-
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
16
|
+
import { layoutSlotTypes, populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
17
17
|
import { blockConversionTargets, prepareBlockConversion } from "./block-convert.js";
|
|
18
18
|
import { checkFormat } from "./checks.js";
|
|
19
19
|
import { listCatalogRecords, mergeCatalogs } from "./catalogs.js";
|
|
@@ -273,7 +273,8 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
273
273
|
if (withRecord.slides[slideIndex].layout !== choice.reference) {
|
|
274
274
|
const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"], choice.reference);
|
|
275
275
|
const swapped = applyJsonPatch(withRecord, layoutOps);
|
|
276
|
-
|
|
276
|
+
// FA-26: a record's placeholder groups contribute their leaf regions, in reading order.
|
|
277
|
+
const types = layoutSlotTypes(record);
|
|
277
278
|
// Add the blank payloads the layout declares, as the JSON editor's layout choice does.
|
|
278
279
|
const populated = types ? populateLayoutPlaceholders(swapped, slideIndex, types) : swapped;
|
|
279
280
|
patches = same(populated.slides[slideIndex], swapped.slides[slideIndex])
|
package/dist/table-options.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface TableCellState extends TableCellAddress {
|
|
|
37
37
|
rows: number;
|
|
38
38
|
}
|
|
39
39
|
export interface PreparedTableChange {
|
|
40
|
-
action: "merge" | "split" | "style" | "table-style";
|
|
40
|
+
action: "merge" | "split" | "style" | "table-style" | "table-alt";
|
|
41
41
|
tablePath: string;
|
|
42
42
|
presentation: unknown;
|
|
43
43
|
patches: JsonPatchOperation[];
|
|
@@ -72,6 +72,11 @@ export declare function mergeTableCells(editor: EditorSession, tablePath: string
|
|
|
72
72
|
export declare function splitTableCell(editor: EditorSession, tablePath: string, cell: TableCellAddress, meta?: Record<string, unknown>): TableChange;
|
|
73
73
|
export declare function setTableCellStyle(editor: EditorSession, tablePath: string, cells: TableCellAddress | TableCellAddress[], style: Record<string, unknown> | null, meta?: Record<string, unknown>): TableChange;
|
|
74
74
|
export declare function setTableStyle(editor: EditorSession, tablePath: string, preset: keyof typeof TABLE_STYLE_PRESETS | Partial<TableStyle>, meta?: Record<string, unknown>): TableChange;
|
|
75
|
+
/** FA-27: the table's text alternative as form state: `alt` (empty when absent or decorative) and `decorative` (the empty alt). */
|
|
76
|
+
export declare function readTableAlt(table: unknown): { alt: string; decorative: boolean };
|
|
77
|
+
/** FA-27: change the table's text alternative. `alt` is trimmed and an empty string or null removes it; `decorative: true` writes the empty alt and wins over `alt`; `decorative: false` removes an empty alt. Works on inline and dataset-backed tables. */
|
|
78
|
+
export declare function prepareTableAlt(presentation: unknown, tablePath: string, change: { alt?: string | null; decorative?: boolean }): PreparedTableChange;
|
|
79
|
+
export declare function setTableAlt(editor: EditorSession, tablePath: string, change: { alt?: string | null; decorative?: boolean }, meta?: Record<string, unknown>): TableChange;
|
|
75
80
|
/** {@link readTableStyle} for a table object. */
|
|
76
81
|
export declare function readTableStyleOfTable(table: unknown): (TableStyle | { header: "custom"; banding: "custom"; borders: "custom" }) & { preset: keyof typeof TABLE_STYLE_PRESETS | "custom" };
|
|
77
82
|
/** Apply a table style to a table object in place (the structure operations use it to keep banding, header fill and borders correct). */
|