@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.
- package/README.md +333 -8
- package/dist/annotations.d.ts +71 -0
- package/dist/annotations.js +281 -0
- package/dist/assets.d.ts +67 -0
- package/dist/assets.js +176 -0
- package/dist/background-options.d.ts +48 -0
- package/dist/background-options.js +134 -0
- package/dist/block-convert.d.ts +64 -0
- package/dist/block-convert.js +142 -0
- package/dist/canvas.d.ts +16 -0
- package/dist/canvas.js +82 -21
- package/dist/chart-data.d.ts +32 -0
- package/dist/chart-data.js +101 -0
- package/dist/chart-options-panel.d.ts +16 -0
- package/dist/chart-options-panel.js +127 -0
- package/dist/chart-options.d.ts +49 -0
- package/dist/chart-options.js +157 -0
- package/dist/content-actions.d.ts +91 -0
- package/dist/content-actions.js +207 -0
- package/dist/content-controls.js +326 -0
- package/dist/data-grid.d.ts +37 -0
- package/dist/data-grid.js +1035 -0
- package/dist/design-controls.d.ts +43 -0
- package/dist/design-controls.js +1077 -0
- package/dist/design-options.d.ts +108 -0
- package/dist/design-options.js +412 -0
- package/dist/edit-helpers.js +52 -0
- package/dist/export.d.ts +77 -0
- package/dist/export.js +216 -0
- package/dist/find-panel.d.ts +44 -0
- package/dist/find-panel.js +431 -0
- package/dist/find-replace.d.ts +100 -0
- package/dist/find-replace.js +374 -0
- package/dist/grid-model.d.ts +135 -0
- package/dist/grid-model.js +836 -0
- package/dist/grid-text.d.ts +33 -0
- package/dist/grid-text.js +251 -0
- package/dist/image-crop.d.ts +59 -0
- package/dist/image-crop.js +336 -0
- package/dist/image-cropper.d.ts +29 -0
- package/dist/image-cropper.js +519 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +104 -171
- package/dist/numbering-panel.d.ts +21 -0
- package/dist/numbering-panel.js +200 -0
- package/dist/numbering.d.ts +62 -0
- package/dist/numbering.js +223 -0
- package/dist/outline-view.d.ts +17 -0
- package/dist/outline-view.js +278 -0
- package/dist/outline.d.ts +56 -0
- package/dist/outline.js +271 -0
- package/dist/persistence-ui.d.ts +24 -0
- package/dist/persistence-ui.js +81 -0
- package/dist/persistence.d.ts +105 -0
- package/dist/persistence.js +429 -0
- package/dist/review-panel.d.ts +44 -0
- package/dist/review-panel.js +359 -0
- package/dist/review.d.ts +75 -0
- package/dist/review.js +170 -0
- package/dist/slide-manager.d.ts +44 -0
- package/dist/slide-manager.js +695 -0
- package/dist/slides.d.ts +96 -0
- package/dist/slides.js +433 -0
- package/dist/switches.d.ts +26 -0
- package/dist/switches.js +127 -43
- package/dist/table-options.d.ts +80 -0
- package/dist/table-options.js +419 -0
- package/dist/table-structure.d.ts +30 -0
- package/dist/table-structure.js +92 -0
- package/dist/template-panel.d.ts +31 -0
- package/dist/template-panel.js +377 -0
- package/dist/templates.d.ts +126 -0
- package/dist/templates.js +331 -0
- package/dist/zip.d.ts +4 -0
- package/dist/zip.js +71 -0
- package/package.json +150 -10
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export type DesignOptionId =
|
|
4
|
+
| "titleAlignment"
|
|
5
|
+
| "contentAlignment"
|
|
6
|
+
| "contentDirection"
|
|
7
|
+
| "chartPrimary"
|
|
8
|
+
| "listBullet"
|
|
9
|
+
| "contentBox"
|
|
10
|
+
| "accentFont"
|
|
11
|
+
| "logo"
|
|
12
|
+
| "organizationLogo"
|
|
13
|
+
| "watermark"
|
|
14
|
+
| "slideImage";
|
|
15
|
+
export interface DesignOptionDescriptor {
|
|
16
|
+
id: DesignOptionId;
|
|
17
|
+
label: string;
|
|
18
|
+
type: "enum" | "boolean" | "font" | "logo" | "organization-logo" | "watermark" | "slide-image";
|
|
19
|
+
/** Allowed values of an enum option. */
|
|
20
|
+
values?: readonly string[];
|
|
21
|
+
scopes: ("deck" | "slide")[];
|
|
22
|
+
path: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const DESIGN_OPTIONS: readonly DesignOptionDescriptor[];
|
|
25
|
+
export declare const LOGO_VARIANTS: readonly ["default", "light", "dark", "stacked", "stackedLight", "stackedDark", "icon", "iconLight", "iconDark", "wordmark", "wordmarkLight", "wordmarkDark"];
|
|
26
|
+
export type LogoVariant = (typeof LOGO_VARIANTS)[number];
|
|
27
|
+
/** Every field a header or footer zone can hold. */
|
|
28
|
+
export declare const ZONE_FIELDS: readonly ["logo", "text", "image", "slideNumber", "slideNumberFormat", "date", "dateFormat", "organization", "socials", "section"];
|
|
29
|
+
/** Date format tokens (English names). */
|
|
30
|
+
export declare const DATE_FORMAT_TOKENS: readonly string[];
|
|
31
|
+
export declare const HEADER_FOOTER_ZONES: readonly ["left", "center", "right"];
|
|
32
|
+
export type HeaderFooterZone = (typeof HEADER_FOOTER_ZONES)[number];
|
|
33
|
+
|
|
34
|
+
export interface DesignOptionOptions {
|
|
35
|
+
/** One slide instead of the deck. Not allowed for organizationLogo. */
|
|
36
|
+
slideIndex?: number;
|
|
37
|
+
/** Deck scope: also remove slide-level values that would hide the change. */
|
|
38
|
+
clearSlideOverrides?: boolean;
|
|
39
|
+
/** organizationLogo with several organizations (default 0). */
|
|
40
|
+
index?: number;
|
|
41
|
+
/** Session change metadata (session forms only). */
|
|
42
|
+
meta?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
export interface DesignWarning {
|
|
45
|
+
code: "unresolved-logo" | "unresolved-content";
|
|
46
|
+
path: string;
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
export interface PreparedDesignOption {
|
|
50
|
+
option: string;
|
|
51
|
+
scope: "deck" | "slide";
|
|
52
|
+
slideIndex?: number;
|
|
53
|
+
/** The logo variant or header/footer zone an edit named. */
|
|
54
|
+
variant?: string;
|
|
55
|
+
zone?: string;
|
|
56
|
+
document: unknown;
|
|
57
|
+
patches: JsonPatchOperation[];
|
|
58
|
+
changed: boolean;
|
|
59
|
+
/** Slides whose own design hides a deck-level change. */
|
|
60
|
+
shadowed: number[];
|
|
61
|
+
/** Settings that need a logo the document does not have. */
|
|
62
|
+
warnings: DesignWarning[];
|
|
63
|
+
}
|
|
64
|
+
export interface DesignOptionChange extends Omit<EditorChange, "document" | "patches"> {
|
|
65
|
+
document: unknown;
|
|
66
|
+
patches: JsonPatchOperation[];
|
|
67
|
+
option: string;
|
|
68
|
+
scope: "deck" | "slide";
|
|
69
|
+
slideIndex?: number;
|
|
70
|
+
changed: boolean;
|
|
71
|
+
shadowed: number[];
|
|
72
|
+
warnings: DesignWarning[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Compute the patch for one option. `null` removes it at that scope; object options (watermark, slideImage) merge the fields passed. */
|
|
76
|
+
export declare function prepareDesignOption(document: unknown, option: DesignOptionId, value: unknown, options?: DesignOptionOptions): PreparedDesignOption;
|
|
77
|
+
/** Set one option as a single undoable transaction. */
|
|
78
|
+
export declare function setDesignOption(editor: EditorSession, option: DesignOptionId, value: unknown, options?: DesignOptionOptions): DesignOptionChange;
|
|
79
|
+
/** `{ value, scope, inherited }`; scope is "slide", "deck" or "default". */
|
|
80
|
+
export declare function getDesignOption(document: unknown, option: DesignOptionId, options?: Pick<DesignOptionOptions, "slideIndex" | "index">): { value: unknown; scope: "slide" | "deck" | "default"; inherited: boolean };
|
|
81
|
+
export declare function prepareLogoVariant(document: unknown, variant: LogoVariant, source: string | Record<string, unknown> | null, options?: DesignOptionOptions): PreparedDesignOption;
|
|
82
|
+
/** Set or clear one `design.logo` variant; a lone default stays a bare source. */
|
|
83
|
+
export declare function setLogoVariant(editor: EditorSession, variant: LogoVariant, source: string | Record<string, unknown> | null, options?: DesignOptionOptions): DesignOptionChange;
|
|
84
|
+
/** The variants `design.logo` sets at a scope (a bare logo is reported as `default`). */
|
|
85
|
+
export declare function readLogoVariants(document: unknown, options?: Pick<DesignOptionOptions, "slideIndex">): Partial<Record<LogoVariant, unknown>>;
|
|
86
|
+
export interface HeaderFooterZoneFields {
|
|
87
|
+
logo?: boolean | null;
|
|
88
|
+
text?: string | null;
|
|
89
|
+
image?: string | Record<string, unknown> | null;
|
|
90
|
+
slideNumber?: boolean | null;
|
|
91
|
+
slideNumberFormat?: string | null;
|
|
92
|
+
date?: boolean | string | null;
|
|
93
|
+
dateFormat?: string | null;
|
|
94
|
+
organization?: boolean | null;
|
|
95
|
+
socials?: boolean | null;
|
|
96
|
+
section?: boolean | null;
|
|
97
|
+
}
|
|
98
|
+
export declare function prepareHeaderFooterZone(document: unknown, which: "header" | "footer", zone: HeaderFooterZone, fields: HeaderFooterZoneFields, options?: DesignOptionOptions): PreparedDesignOption;
|
|
99
|
+
/** Merge fields into one header or footer zone; null, false (flags) or "" remove a field, an empty zone and header are removed. A slide's own header replaces the deck's whole one, so the first edit on a slide starts from a copy of the deck's and keeps its other zones. */
|
|
100
|
+
export declare function setHeaderFooterZone(editor: EditorSession, which: "header" | "footer", zone: HeaderFooterZone, fields: HeaderFooterZoneFields, options?: DesignOptionOptions): DesignOptionChange;
|
|
101
|
+
/** One zone's fields as they apply at a scope: the slide's own header or footer when it has one, else the deck's. */
|
|
102
|
+
export declare function readHeaderFooterZone(document: unknown, which: "header" | "footer", zone: HeaderFooterZone, options?: Pick<DesignOptionOptions, "slideIndex">): HeaderFooterZoneFields;
|
|
103
|
+
/** Whether the scope sets the header or footer itself (`own`), shows the deck's (`inherited`) or hides it with `false` (`hidden`). */
|
|
104
|
+
export declare function headerFooterState(document: unknown, which: "header" | "footer", options?: Pick<DesignOptionOptions, "slideIndex">): { own: boolean; inherited: boolean; hidden: boolean };
|
|
105
|
+
/** Whether a logo resolves for the slide: slide design, deck design, then the primary organization. */
|
|
106
|
+
export declare function hasResolvableLogo(document: unknown, slideIndex: number): boolean;
|
|
107
|
+
/** Settings that need content the document does not have: a logo (zones with `logo: true`, picture bullets), or an organization or its social profiles for zones that show them. */
|
|
108
|
+
export declare function designWarnings(document: unknown, slideIndex?: number): DesignWarning[];
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// Design-level options (RR-06): the settings the All-properties workspace used to be the only
|
|
2
|
+
// place to edit. Each is one validated JSON Patch applied as one undoable transaction, at the deck
|
|
3
|
+
// by default or on one slide with `slideIndex`, exactly like the dimension switches. Every
|
|
4
|
+
// function has a `prepare…` form that returns the patch without touching a session, and a
|
|
5
|
+
// session form that commits it with `meta.source: "design-option"`.
|
|
6
|
+
import { getValueAtPath, opfPathToJsonPointer, validateOpfDocument } from "./index.js";
|
|
7
|
+
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
8
|
+
|
|
9
|
+
/** The `design.logo` variant slots of a LogoSet, in schema order. */
|
|
10
|
+
export const LOGO_VARIANTS = Object.freeze([
|
|
11
|
+
"default",
|
|
12
|
+
"light",
|
|
13
|
+
"dark",
|
|
14
|
+
"stacked",
|
|
15
|
+
"stackedLight",
|
|
16
|
+
"stackedDark",
|
|
17
|
+
"icon",
|
|
18
|
+
"iconLight",
|
|
19
|
+
"iconDark",
|
|
20
|
+
"wordmark",
|
|
21
|
+
"wordmarkLight",
|
|
22
|
+
"wordmarkDark",
|
|
23
|
+
]);
|
|
24
|
+
export const HEADER_FOOTER_ZONES = Object.freeze(["left", "center", "right"]);
|
|
25
|
+
// Flag fields of a header/footer zone: false is stored as "absent" (so is a `date` of false).
|
|
26
|
+
const ZONE_FLAGS = ["logo", "slideNumber", "organization", "socials", "section"];
|
|
27
|
+
export const ZONE_FIELDS = Object.freeze(["logo", "text", "image", "slideNumber", "slideNumberFormat", "date", "dateFormat", "organization", "socials", "section"]);
|
|
28
|
+
/** Date tokens a `dateFormat` understands (English names, independent of the host locale). */
|
|
29
|
+
export const DATE_FORMAT_TOKENS = Object.freeze(["yyyy", "yy", "MMMM", "MMM", "MM", "M", "dd", "d", "EEEE", "EEE"]);
|
|
30
|
+
const ISO_DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
31
|
+
|
|
32
|
+
// Friendly validation of one zone field before the schema sees it.
|
|
33
|
+
function checkZoneField(key, value) {
|
|
34
|
+
const bad = (message) => fail("invalid-design-value", message, { field: key, value });
|
|
35
|
+
if (["logo", "slideNumber", "organization", "socials", "section"].includes(key) && typeof value !== "boolean") throw bad(`${key} is true or false.`);
|
|
36
|
+
if (key === "text" && typeof value !== "string") throw bad("Text is a string.");
|
|
37
|
+
if (key === "image" && typeof value !== "string" && !isObject(value)) throw bad("An image is a source, an asset reference or an asset object.");
|
|
38
|
+
if (key === "slideNumberFormat" && (typeof value !== "string" || !value.includes("{current}"))) throw bad("The slide number format must contain {current}, for example Page {current} of {total}.");
|
|
39
|
+
if (key === "date" && typeof value !== "boolean" && typeof value !== "string") throw bad("A date is true (the current date) or a fixed date.");
|
|
40
|
+
if (key === "dateFormat" && (typeof value !== "string" || !value.trim())) throw bad(`A date format uses tokens such as ${DATE_FORMAT_TOKENS.join(", ")}.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SLIDE_IMAGE_POSITIONS = ["background", "top", "bottom", "left", "right"];
|
|
44
|
+
|
|
45
|
+
const ENUMS = Object.freeze({
|
|
46
|
+
titleAlignment: ["left", "center", "right"],
|
|
47
|
+
contentAlignment: ["left", "center", "right"],
|
|
48
|
+
contentDirection: ["horizontal", "vertical"],
|
|
49
|
+
chartPrimary: ["none", "top", "bottom", "left", "right"],
|
|
50
|
+
listBullet: ["character", "image"],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Descriptors for every design option, in the order a panel shows them. `type` is "enum", "boolean",
|
|
55
|
+
* "font", "asset", "logo", "watermark", "slide-image" or "organization-logo"; `scopes` says whether
|
|
56
|
+
* the option applies to the deck, one slide, or both.
|
|
57
|
+
*/
|
|
58
|
+
export const DESIGN_OPTIONS = Object.freeze([
|
|
59
|
+
{ id: "titleAlignment", label: "Title alignment", type: "enum", values: ENUMS.titleAlignment, scopes: ["deck", "slide"], path: "design.titleAlignment" },
|
|
60
|
+
{ id: "contentAlignment", label: "Content alignment", type: "enum", values: ENUMS.contentAlignment, scopes: ["deck", "slide"], path: "design.contentAlignment" },
|
|
61
|
+
{ id: "contentDirection", label: "Content direction", type: "enum", values: ENUMS.contentDirection, scopes: ["deck", "slide"], path: "design.contentDirection" },
|
|
62
|
+
{ id: "chartPrimary", label: "Primary chart position", type: "enum", values: ENUMS.chartPrimary, scopes: ["deck", "slide"], path: "design.chartPrimary" },
|
|
63
|
+
{ id: "listBullet", label: "List bullets", type: "enum", values: ENUMS.listBullet, scopes: ["deck", "slide"], path: "design.listBullet" },
|
|
64
|
+
{ id: "contentBox", label: "Content box", type: "boolean", scopes: ["deck", "slide"], path: "design.contentBox" },
|
|
65
|
+
{ id: "accentFont", label: "Accent font", type: "font", scopes: ["deck", "slide"], path: "design.fontScheme.accent.family" },
|
|
66
|
+
{ id: "logo", label: "Logo", type: "logo", scopes: ["deck", "slide"], path: "design.logo" },
|
|
67
|
+
{ id: "organizationLogo", label: "Organization logo", type: "organization-logo", scopes: ["deck"], path: "organization.logo" },
|
|
68
|
+
{ id: "watermark", label: "Watermark", type: "watermark", scopes: ["deck", "slide"], path: "design.watermark" },
|
|
69
|
+
{ id: "slideImage", label: "Slide image", type: "slide-image", scopes: ["deck", "slide"], path: "design.slideImage" },
|
|
70
|
+
]);
|
|
71
|
+
const BY_ID = Object.fromEntries(DESIGN_OPTIONS.map((option) => [option.id, option]));
|
|
72
|
+
|
|
73
|
+
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
74
|
+
|
|
75
|
+
function scopeOf(document, option, options) {
|
|
76
|
+
const index = options.slideIndex;
|
|
77
|
+
if (index === undefined) return { scope: "deck", base: [] };
|
|
78
|
+
if (!BY_ID[option]?.scopes.includes("slide")) throw fail("invalid-scope", `${BY_ID[option]?.label ?? option} applies to the whole presentation, not one slide.`, { option });
|
|
79
|
+
if (!Number.isInteger(index) || !document.slides?.[index]) throw fail("slide-index-out-of-range", "Choose an existing slide for this option.", { slideIndex: index });
|
|
80
|
+
return { scope: "slide", base: ["slides", String(index)], slideIndex: index };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ownDesign(document, base) {
|
|
84
|
+
const design = getValueAtPath(document, base.length ? [...base, "design"] : ["design"]);
|
|
85
|
+
return isObject(design) ? design : {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function primaryOrganization(document) {
|
|
89
|
+
const list = Array.isArray(document.organization) ? document.organization : document.organization ? [document.organization] : [];
|
|
90
|
+
return list.find((entry) => entry?.role === "primary") ?? list[0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether a logo resolves for the slide: slide design, then deck design, then the primary organization. */
|
|
94
|
+
export function hasResolvableLogo(document, slideIndex) {
|
|
95
|
+
return Boolean(document.slides?.[slideIndex]?.design?.logo ?? document.design?.logo ?? primaryOrganization(document)?.logo);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Plain-language warnings for settings that need a logo the document does not have: a header or
|
|
100
|
+
* footer zone with `logo: true`, or picture bullets. The renderers fall back to a glyph or report
|
|
101
|
+
* unresolved content; the editor surfaces it before export.
|
|
102
|
+
*/
|
|
103
|
+
export function designWarnings(document, slideIndex = 0) {
|
|
104
|
+
const slide = document.slides?.[slideIndex];
|
|
105
|
+
const design = { ...document.design, ...slide?.design };
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const hasLogo = hasResolvableLogo(document, slideIndex);
|
|
108
|
+
for (const which of ["header", "footer"])
|
|
109
|
+
for (const zone of HEADER_FOOTER_ZONES)
|
|
110
|
+
if (!hasLogo && isObject(design[which]) && design[which][zone]?.logo === true)
|
|
111
|
+
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.` });
|
|
112
|
+
const organization = primaryOrganization(document);
|
|
113
|
+
for (const which of ["header", "footer"])
|
|
114
|
+
for (const zone of HEADER_FOOTER_ZONES) {
|
|
115
|
+
const item = isObject(design[which]) ? design[which][zone] : undefined;
|
|
116
|
+
if (item?.organization === true && !organization)
|
|
117
|
+
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.organization`, message: `The ${which} ${zone} zone shows the organization, but the presentation has none.` });
|
|
118
|
+
if (item?.socials === true && !organization?.socials)
|
|
119
|
+
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.socials`, message: `The ${which} ${zone} zone shows social profiles, but the organization has none.` });
|
|
120
|
+
}
|
|
121
|
+
if (!hasLogo && design.listBullet === "image")
|
|
122
|
+
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." });
|
|
123
|
+
return warnings;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function shadowed(document, keys) {
|
|
127
|
+
return (document.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function finish(document, patches, extra) {
|
|
131
|
+
const before = validateOpfDocument(document);
|
|
132
|
+
const next = checkedDocument(document, patches, before);
|
|
133
|
+
const slideIndex = extra.slideIndex;
|
|
134
|
+
return {
|
|
135
|
+
...extra,
|
|
136
|
+
document: structuredClone(next),
|
|
137
|
+
patches,
|
|
138
|
+
changed: patches.length > 0,
|
|
139
|
+
warnings: designWarnings(next, slideIndex ?? 0),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- per-option value rules ---------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
function mergedWatermark(existing, value) {
|
|
146
|
+
if (value === null || value === false) return value;
|
|
147
|
+
if (typeof value === "string") return value;
|
|
148
|
+
if (!isObject(value)) throw fail("invalid-design-value", "A watermark is false, an image source or { src, opacity }.", { value });
|
|
149
|
+
const base = typeof existing === "string" ? { src: existing } : isObject(existing) ? { ...existing } : {};
|
|
150
|
+
const merged = { ...base };
|
|
151
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
152
|
+
if (entry === null || entry === undefined) delete merged[key];
|
|
153
|
+
else merged[key] = entry;
|
|
154
|
+
}
|
|
155
|
+
if (merged.opacity !== undefined && (typeof merged.opacity !== "number" || merged.opacity < 0 || merged.opacity > 1))
|
|
156
|
+
throw fail("invalid-design-value", "Watermark opacity is a number from 0 to 1.", { value });
|
|
157
|
+
if (merged.src === undefined && Object.keys(merged).length) throw fail("invalid-design-value", "Choose the watermark image before setting its opacity.", { value });
|
|
158
|
+
if (Object.keys(merged).length === 1 && typeof merged.src === "string") return merged.src;
|
|
159
|
+
if (!Object.keys(merged).length) return null;
|
|
160
|
+
if (merged.opacity === undefined) throw fail("invalid-design-value", "Set an opacity from 0 to 1 for the watermark.", { value });
|
|
161
|
+
return merged;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mergedSlideImage(existing, value) {
|
|
165
|
+
if (value === null) return null;
|
|
166
|
+
if (typeof value === "string") return value;
|
|
167
|
+
if (!isObject(value)) throw fail("invalid-design-value", "A slide image is an image source or an object with a position.", { value });
|
|
168
|
+
const base = typeof existing === "string" ? { src: existing } : isObject(existing) ? (existing.position ? { ...existing } : { src: existing.src, ...(existing.alt ? { alt: existing.alt } : {}) }) : {};
|
|
169
|
+
const merged = { ...base };
|
|
170
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
171
|
+
if (entry === null || entry === undefined) delete merged[key];
|
|
172
|
+
else merged[key] = entry;
|
|
173
|
+
}
|
|
174
|
+
if (merged.src === undefined) delete merged.src;
|
|
175
|
+
if (merged.position === undefined) merged.position = "background";
|
|
176
|
+
if (!SLIDE_IMAGE_POSITIONS.includes(merged.position)) throw fail("invalid-design-value", `Slide image position is one of ${SLIDE_IMAGE_POSITIONS.join(", ")}.`, { value });
|
|
177
|
+
return merged;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function fontSchemeWithAccent(document, base, family) {
|
|
181
|
+
// The slide's own font scheme, else the deck's: an object form is kept with its overrides.
|
|
182
|
+
const own = ownDesign(document, base).fontScheme;
|
|
183
|
+
const inherited = base.length ? document.design?.fontScheme : undefined;
|
|
184
|
+
const source = own !== undefined ? own : inherited;
|
|
185
|
+
const object = typeof source === "string" ? { id: source } : isObject(source) ? structuredClone(source) : {};
|
|
186
|
+
if (family === null) {
|
|
187
|
+
delete object.accent;
|
|
188
|
+
} else {
|
|
189
|
+
if (typeof family !== "string" || !family.trim()) throw fail("invalid-design-value", "Accent font is a non-empty family name, or null to clear it.", { value: family });
|
|
190
|
+
object.accent = { ...(isObject(object.accent) ? object.accent : {}), family: family.trim() };
|
|
191
|
+
}
|
|
192
|
+
const keys = Object.keys(object);
|
|
193
|
+
if (!keys.length) return null;
|
|
194
|
+
if (keys.length === 1 && keys[0] === "id") return object.id;
|
|
195
|
+
return object;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function logoSetOf(existing) {
|
|
199
|
+
if (existing === undefined) return {};
|
|
200
|
+
if (typeof existing === "string" || (isObject(existing) && Object.hasOwn(existing, "src"))) return { default: existing };
|
|
201
|
+
return isObject(existing) ? { ...existing } : {};
|
|
202
|
+
}
|
|
203
|
+
function collapseLogo(set) {
|
|
204
|
+
const keys = Object.keys(set);
|
|
205
|
+
if (!keys.length) return null;
|
|
206
|
+
if (keys.length === 1 && keys[0] === "default") return set.default;
|
|
207
|
+
return set;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- design option ----------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Compute the patch that sets one design option, without touching any session. `value === null`
|
|
214
|
+
* removes the option at that scope so it is inherited again. Object-valued options (`watermark`,
|
|
215
|
+
* `slideImage`) merge the fields you pass into the existing object; a `null` field removes it.
|
|
216
|
+
* Options: `slideIndex` (one slide instead of the deck), `clearSlideOverrides` (deck scope: also
|
|
217
|
+
* remove slide-level values that hide it) and `index` (organizationLogo with several
|
|
218
|
+
* organizations).
|
|
219
|
+
*/
|
|
220
|
+
export function prepareDesignOption(document, option, value, options = {}) {
|
|
221
|
+
const descriptor = BY_ID[option];
|
|
222
|
+
if (!descriptor) throw fail("unknown-design-option", `Unknown design option: ${option}. Use one of ${DESIGN_OPTIONS.map((entry) => entry.id).join(", ")}.`, { option });
|
|
223
|
+
if (!isObject(document)) throw fail("invalid-input", "Design options need an OPF document object.");
|
|
224
|
+
if (value === undefined) throw fail("invalid-design-value", "Pass a value, or null to remove the option.", { option });
|
|
225
|
+
const { scope, base, slideIndex } = scopeOf(document, option, options);
|
|
226
|
+
let patches = [];
|
|
227
|
+
let keys = [];
|
|
228
|
+
|
|
229
|
+
if (descriptor.type === "organization-logo") {
|
|
230
|
+
const organization = document.organization;
|
|
231
|
+
const index = options.index ?? 0;
|
|
232
|
+
const owner = Array.isArray(organization) ? organization[index] : organization;
|
|
233
|
+
if (!isObject(owner)) throw fail("missing-owner", "Add an organization to the document before setting its logo.", { option });
|
|
234
|
+
const path = Array.isArray(organization) ? ["organization", String(index), "logo"] : ["organization", "logo"];
|
|
235
|
+
const present = Object.hasOwn(owner, "logo");
|
|
236
|
+
if (value === null) patches = present ? [{ op: "remove", path: opfPathToJsonPointer(path) }] : [];
|
|
237
|
+
else if (!present) patches = [{ op: "add", path: opfPathToJsonPointer(path), value: structuredClone(value) }];
|
|
238
|
+
else if (!same(owner.logo, value)) patches = [{ op: "replace", path: opfPathToJsonPointer(path), value: structuredClone(value) }];
|
|
239
|
+
} else {
|
|
240
|
+
let entries;
|
|
241
|
+
if (descriptor.type === "enum") {
|
|
242
|
+
if (value !== null && !ENUMS[option].includes(value)) throw fail("invalid-design-value", `${descriptor.label} is one of ${ENUMS[option].join(", ")}.`, { option, value });
|
|
243
|
+
entries = { [option]: value };
|
|
244
|
+
} else if (descriptor.type === "boolean") {
|
|
245
|
+
if (value !== null && typeof value !== "boolean") throw fail("invalid-design-value", `${descriptor.label} is true or false.`, { option, value });
|
|
246
|
+
entries = { [option]: value };
|
|
247
|
+
} else if (descriptor.type === "font") {
|
|
248
|
+
entries = { fontScheme: fontSchemeWithAccent(document, base, value) };
|
|
249
|
+
} else if (descriptor.type === "logo") {
|
|
250
|
+
if (value !== null && typeof value !== "string" && !isObject(value)) throw fail("invalid-design-value", "A logo is an image source, an asset object or a set of logo variants.", { option });
|
|
251
|
+
entries = { logo: value };
|
|
252
|
+
} else if (descriptor.type === "watermark") {
|
|
253
|
+
const existing = ownDesign(document, base).watermark;
|
|
254
|
+
entries = { watermark: mergedWatermark(existing, value) };
|
|
255
|
+
} else {
|
|
256
|
+
const existing = ownDesign(document, base).slideImage;
|
|
257
|
+
entries = { slideImage: mergedSlideImage(existing, value) };
|
|
258
|
+
}
|
|
259
|
+
keys = Object.keys(entries);
|
|
260
|
+
patches = designPatches(document, base, entries);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let shadowedSlides = [];
|
|
264
|
+
if (scope === "deck" && keys.length) {
|
|
265
|
+
shadowedSlides = shadowed(document, keys);
|
|
266
|
+
if (options.clearSlideOverrides) {
|
|
267
|
+
for (const index of shadowedSlides)
|
|
268
|
+
for (const key of keys)
|
|
269
|
+
if (document.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
|
|
270
|
+
shadowedSlides = [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return finish(document, patches, { option, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: shadowedSlides });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function apply(editor, prepared, meta = {}) {
|
|
277
|
+
const { document, patches, ...summary } = prepared;
|
|
278
|
+
void document;
|
|
279
|
+
if (!prepared.changed) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
|
|
280
|
+
const change = editor.applyPatch(patches, { ...meta, source: meta.source ?? "design-option", option: prepared.option, scope: prepared.scope });
|
|
281
|
+
return { ...change, ...summary };
|
|
282
|
+
}
|
|
283
|
+
function checkEditor(editor) {
|
|
284
|
+
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
|
|
285
|
+
throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Set one design option as a single undoable transaction. See {@link prepareDesignOption}. */
|
|
289
|
+
export function setDesignOption(editor, option, value, options = {}) {
|
|
290
|
+
checkEditor(editor);
|
|
291
|
+
const { meta, ...rest } = options;
|
|
292
|
+
return apply(editor, prepareDesignOption(editor.document, option, value, rest), meta);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Read one design option for a panel: `{ value, scope, inherited }` where `scope` is "slide" when the
|
|
297
|
+
* slide's own design sets it, "deck" when the deck does, and "default" when neither does.
|
|
298
|
+
* `accentFont` reads the family; `organizationLogo` reads the organization.
|
|
299
|
+
*/
|
|
300
|
+
export function getDesignOption(document, option, options = {}) {
|
|
301
|
+
const descriptor = BY_ID[option];
|
|
302
|
+
if (!descriptor) throw fail("unknown-design-option", `Unknown design option: ${option}.`, { option });
|
|
303
|
+
if (descriptor.type === "organization-logo") {
|
|
304
|
+
const organization = document.organization;
|
|
305
|
+
const owner = Array.isArray(organization) ? organization[options.index ?? 0] : organization;
|
|
306
|
+
return { value: owner?.logo, scope: owner?.logo === undefined ? "default" : "deck", inherited: false };
|
|
307
|
+
}
|
|
308
|
+
const key = descriptor.type === "font" ? "fontScheme" : option;
|
|
309
|
+
const pick = (design) => {
|
|
310
|
+
const value = design?.[key];
|
|
311
|
+
return descriptor.type === "font" ? (isObject(value) ? value.accent?.family : undefined) : value;
|
|
312
|
+
};
|
|
313
|
+
const slideValue = options.slideIndex === undefined ? undefined : pick(document.slides?.[options.slideIndex]?.design);
|
|
314
|
+
if (slideValue !== undefined) return { value: slideValue, scope: "slide", inherited: false };
|
|
315
|
+
const deckValue = pick(document.design);
|
|
316
|
+
if (deckValue !== undefined) return { value: deckValue, scope: "deck", inherited: options.slideIndex !== undefined };
|
|
317
|
+
return { value: undefined, scope: "default", inherited: false };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- logo variants ----------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Compute the patch that sets (a source, asset reference or Asset object) or clears (`null`) one logo
|
|
324
|
+
* variant of `design.logo`. A single default logo stays a bare source; adding a second variant turns
|
|
325
|
+
* it into a LogoSet, and clearing back to the default collapses it again.
|
|
326
|
+
*/
|
|
327
|
+
export function prepareLogoVariant(document, variant, source, options = {}) {
|
|
328
|
+
if (!LOGO_VARIANTS.includes(variant)) throw fail("invalid-design-value", `Logo variant is one of ${LOGO_VARIANTS.join(", ")}.`, { variant });
|
|
329
|
+
if (source !== null && typeof source !== "string" && !isObject(source)) throw fail("invalid-design-value", "A logo variant is an image source, an asset object, or null to clear it.", { variant });
|
|
330
|
+
if (typeof source === "string" && !source.trim()) throw fail("invalid-design-value", "Enter an image source or asset reference, or clear the variant.", { variant });
|
|
331
|
+
const { scope, base, slideIndex } = scopeOf(document, "logo", options);
|
|
332
|
+
const set = logoSetOf(ownDesign(document, base).logo);
|
|
333
|
+
if (source === null) delete set[variant];
|
|
334
|
+
else set[variant] = typeof source === "string" ? source.trim() : source;
|
|
335
|
+
const patches = designPatches(document, base, { logo: collapseLogo(set) });
|
|
336
|
+
return finish(document, patches, { option: "logo", variant, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
|
|
337
|
+
}
|
|
338
|
+
/** Set or clear one logo variant as a single undoable transaction. */
|
|
339
|
+
export function setLogoVariant(editor, variant, source, options = {}) {
|
|
340
|
+
checkEditor(editor);
|
|
341
|
+
const { meta, ...rest } = options;
|
|
342
|
+
return apply(editor, prepareLogoVariant(editor.document, variant, source, rest), meta);
|
|
343
|
+
}
|
|
344
|
+
/** The variants `design.logo` sets at a scope: `{ variant: source }`, a bare logo reported as `default`. */
|
|
345
|
+
export function readLogoVariants(document, options = {}) {
|
|
346
|
+
const own = options.slideIndex === undefined ? document.design?.logo : document.slides?.[options.slideIndex]?.design?.logo;
|
|
347
|
+
return logoSetOf(own);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- header and footer zones ------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Compute the patch that edits one header or footer zone. `fields` merges into the zone (logo, text,
|
|
354
|
+
* image, slideNumber, slideNumberFormat, date, dateFormat, organization, socials, section); `null`,
|
|
355
|
+
* `false` for a flag, or an empty string removes a field. A zone left empty is removed, then an empty
|
|
356
|
+
* header or footer, so a slide never carries `{}`. A slide's own header or footer replaces the deck's
|
|
357
|
+
* whole one, so the first edit on a slide that has none of its own starts from a copy of the deck's
|
|
358
|
+
* (the other zones stay); a slide emptied that way hides the furniture (`false`) instead of
|
|
359
|
+
* inheriting it again. Setting a field on a suppressed (`false`) header replaces the suppression.
|
|
360
|
+
* `logo: true` reports a warning when no logo resolves.
|
|
361
|
+
*/
|
|
362
|
+
export function prepareHeaderFooterZone(document, which, zone, fields, options = {}) {
|
|
363
|
+
if (!["header", "footer"].includes(which)) throw fail("invalid-design-value", "Choose header or footer.", { which });
|
|
364
|
+
if (!HEADER_FOOTER_ZONES.includes(zone)) throw fail("invalid-design-value", `Zone is one of ${HEADER_FOOTER_ZONES.join(", ")}.`, { zone });
|
|
365
|
+
if (!isObject(fields)) throw fail("invalid-design-value", "Pass the zone fields to change.", { fields });
|
|
366
|
+
const unknown = Object.keys(fields).filter((key) => !ZONE_FIELDS.includes(key));
|
|
367
|
+
if (unknown.length) throw fail("invalid-design-value", `Unknown header/footer field: ${unknown[0]}.`, { fields });
|
|
368
|
+
const { scope, base, slideIndex } = scopeOf(document, "logo", options);
|
|
369
|
+
const own = ownDesign(document, base)[which];
|
|
370
|
+
const inherited = base.length ? document.design?.[which] : undefined;
|
|
371
|
+
const current = own !== undefined ? own : inherited;
|
|
372
|
+
const container = isObject(current) ? structuredClone(current) : {};
|
|
373
|
+
const item = isObject(container[zone]) ? container[zone] : {};
|
|
374
|
+
for (const [key, entry] of Object.entries(fields)) {
|
|
375
|
+
const removes = entry === null || entry === undefined || ((ZONE_FLAGS.includes(key) || key === "date") && entry === false) || entry === "";
|
|
376
|
+
if (removes) delete item[key];
|
|
377
|
+
else {
|
|
378
|
+
checkZoneField(key, entry);
|
|
379
|
+
item[key] = entry;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// A fixed date with a format must be an ISO date (the schema's rule); say so before the generic error.
|
|
383
|
+
if (typeof item.date === "string" && item.dateFormat && !ISO_DATE.test(item.date)) throw fail("invalid-design-value", "A fixed date with a date format must be written YYYY-MM-DD, for example 2026-10-01.", { field: "date", value: item.date });
|
|
384
|
+
if (Object.keys(item).length) container[zone] = item;
|
|
385
|
+
else delete container[zone];
|
|
386
|
+
let next = Object.keys(container).length ? container : null;
|
|
387
|
+
// Emptied: a deck value that would show through again is hidden explicitly; a suppressed header stays suppressed.
|
|
388
|
+
if (next === null && (own === false || isObject(inherited))) next = false;
|
|
389
|
+
const patches = designPatches(document, base, { [which]: next });
|
|
390
|
+
return finish(document, patches, { option: which, zone, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
|
|
391
|
+
}
|
|
392
|
+
/** Edit one header or footer zone as a single undoable transaction. */
|
|
393
|
+
export function setHeaderFooterZone(editor, which, zone, fields, options = {}) {
|
|
394
|
+
checkEditor(editor);
|
|
395
|
+
const { meta, ...rest } = options;
|
|
396
|
+
return apply(editor, prepareHeaderFooterZone(editor.document, which, zone, fields, rest), meta);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* One header or footer zone's fields as they apply at a scope: the slide's own header (or footer) when it
|
|
400
|
+
* has one, else the deck's (`{}` when absent or suppressed). `headerFooterState` says which.
|
|
401
|
+
*/
|
|
402
|
+
export function readHeaderFooterZone(document, which, zone, options = {}) {
|
|
403
|
+
const own = options.slideIndex === undefined ? undefined : document.slides?.[options.slideIndex]?.design?.[which];
|
|
404
|
+
const effective = own !== undefined ? own : document.design?.[which];
|
|
405
|
+
return isObject(effective) && isObject(effective[zone]) ? structuredClone(effective[zone]) : {};
|
|
406
|
+
}
|
|
407
|
+
/** `{ own, inherited, hidden }` for a header or footer at a scope: whether the scope sets it itself, shows the deck's, or hides it with `false`. */
|
|
408
|
+
export function headerFooterState(document, which, options = {}) {
|
|
409
|
+
const own = options.slideIndex === undefined ? document.design?.[which] : document.slides?.[options.slideIndex]?.design?.[which];
|
|
410
|
+
const inherited = options.slideIndex !== undefined && own === undefined && document.design?.[which] !== undefined;
|
|
411
|
+
return { own: own !== undefined, inherited, hidden: (own !== undefined ? own : options.slideIndex !== undefined ? document.design?.[which] : undefined) === false };
|
|
412
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Helpers shared by the dimension switches, the design options, the block conversions and the
|
|
2
|
+
// table options: the error constructor, structural equality and the design-key patch rules.
|
|
3
|
+
import { OPFEditorError, applyJsonPatch, getValueAtPath, opfPathToJsonPointer, validateOpfDocument } from "./index.js";
|
|
4
|
+
|
|
5
|
+
export function fail(code, message, details) {
|
|
6
|
+
return new OPFEditorError(code, message, details);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function canonical(value) {
|
|
10
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
11
|
+
if (value && typeof value === "object")
|
|
12
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Structural equality that ignores object key order. */
|
|
17
|
+
export function same(a, b) {
|
|
18
|
+
return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Set (value) or remove (null) design keys at deck or slide scope.
|
|
22
|
+
export function designPatches(document, base, entries) {
|
|
23
|
+
const design = getValueAtPath(document, base.length ? [...base, "design"] : ["design"]);
|
|
24
|
+
const at = (key) => opfPathToJsonPointer([...base, "design", key]);
|
|
25
|
+
const set = Object.entries(entries).filter(([, value]) => value !== undefined && value !== null);
|
|
26
|
+
if (!design || typeof design !== "object" || Array.isArray(design))
|
|
27
|
+
return set.length ? [{ op: "add", path: opfPathToJsonPointer([...base, "design"]), value: structuredClone(Object.fromEntries(set)) }] : [];
|
|
28
|
+
const patches = [];
|
|
29
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
30
|
+
if (value === undefined) continue;
|
|
31
|
+
const present = Object.hasOwn(design, key);
|
|
32
|
+
if (value === null) {
|
|
33
|
+
if (present) patches.push({ op: "remove", path: at(key) });
|
|
34
|
+
} else if (!present) patches.push({ op: "add", path: at(key), value: structuredClone(value) });
|
|
35
|
+
else if (!same(design[key], value)) patches.push({ op: "replace", path: at(key), value: structuredClone(value) });
|
|
36
|
+
}
|
|
37
|
+
return patches;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate a candidate patch the way every switch does: the result must be valid OPF unless the
|
|
42
|
+
* input document was already invalid (then nothing new may be reported as the cause).
|
|
43
|
+
*/
|
|
44
|
+
export function checkedDocument(document, patches, before) {
|
|
45
|
+
const next = patches.length ? applyJsonPatch(document, patches) : document;
|
|
46
|
+
if (patches.length) {
|
|
47
|
+
const validation = validateOpfDocument(next);
|
|
48
|
+
if (!validation.valid && before.valid)
|
|
49
|
+
throw fail("invalid-opf-edit", validation.errors[0]?.message ?? "This change produces an invalid document.", { issues: validation.errors, patches });
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
}
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { RenderSvgOptions } from "@openpresentation/opf-render";
|
|
2
|
+
import type { FontGate } from "./canvas.js";
|
|
3
|
+
|
|
4
|
+
export type ExportFormat = "pdf" | "png" | "svg";
|
|
5
|
+
export declare const EXPORT_FORMATS: Readonly<Record<ExportFormat, { extension: string; type: string; label: string }>>;
|
|
6
|
+
/** Largest PNG scale offered (4: a 1280 x 720 slide is 5120 x 2880 pixels). */
|
|
7
|
+
export declare const MAX_PNG_SCALE: 4;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The download name: the deck's `filename` (a trailing .pptx, .pdf, .png or .svg dropped), else the slugified `name`, else "presentation",
|
|
11
|
+
* then `suffix` and `.extension`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function exportFileName(deck: { filename?: string; name?: string } | undefined, extension: string, suffix?: string): string;
|
|
14
|
+
/** Slide numbers (0-based) an export covers; hidden slides are skipped in an "all" export unless `includeHidden`. */
|
|
15
|
+
export declare function slidesToExport(deck: { slides?: Array<{ hidden?: boolean }> }, options?: { slides?: "current" | "all" | number[]; slideIndex?: number; includeHidden?: boolean }): number[];
|
|
16
|
+
/** The registry's faces marked to embed only where a slide draws them; a face with a non-permissive license text is left out (`onSkip` is told). */
|
|
17
|
+
export declare function embeddableFonts(registry: object | undefined, onSkip?: (face: { family: string; weight: number; italic?: boolean; license?: string }) => void): Array<Record<string, unknown>>;
|
|
18
|
+
|
|
19
|
+
export interface ExportDiagnostic {
|
|
20
|
+
code: string;
|
|
21
|
+
severity: "info" | "warning";
|
|
22
|
+
message: string;
|
|
23
|
+
/** 0-based slide number, when the note belongs to one slide. */
|
|
24
|
+
slide?: number;
|
|
25
|
+
path?: string;
|
|
26
|
+
family?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function describeDiagnostic(diagnostic: { code?: string; message?: string; path?: string; family?: string; [key: string]: unknown }, source?: "render" | "pdf"): ExportDiagnostic;
|
|
29
|
+
|
|
30
|
+
export interface ExportProgress {
|
|
31
|
+
stage: "fonts" | "render" | "convert" | "archive" | "done";
|
|
32
|
+
done: number;
|
|
33
|
+
total: number;
|
|
34
|
+
message?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ExportFile { name: string; type: string; bytes: Uint8Array }
|
|
37
|
+
export interface ExportOptions {
|
|
38
|
+
format: ExportFormat;
|
|
39
|
+
/** "all" (default), "current" (with `slideIndex`) or slide numbers (0-based). */
|
|
40
|
+
slides?: "current" | "all" | number[];
|
|
41
|
+
slideIndex?: number;
|
|
42
|
+
includeHidden?: boolean;
|
|
43
|
+
/** "vector" (default): selectable text and vector shapes; "raster": an image per slide. */
|
|
44
|
+
pdfMode?: "vector" | "raster";
|
|
45
|
+
/** PNG pixel density (and the raster PDF's), 1 to 4; default 2. */
|
|
46
|
+
scale?: number;
|
|
47
|
+
/** The options the host draws its preview with (`textMeasurement`, `catalogs`, ...). */
|
|
48
|
+
renderOptions?: RenderSvgOptions;
|
|
49
|
+
/** A font gate: the faces the deck needs load before anything is drawn. */
|
|
50
|
+
fonts?: Pick<FontGate, "pending" | "ensure">;
|
|
51
|
+
/** The browser font registry; its faces are what gets embedded. */
|
|
52
|
+
registry?: object;
|
|
53
|
+
/** Faces to embed instead of the registry's. */
|
|
54
|
+
embeddedFonts?: Array<Record<string, unknown>>;
|
|
55
|
+
/** PDF document properties beyond the title and subject taken from the deck. */
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
/** The family the PDF uses when a requested one is missing; default "Roboto". */
|
|
58
|
+
fallbackFamily?: string;
|
|
59
|
+
/** Replace the renderer's `export-browser` entry (tests, or a host with its own converter). */
|
|
60
|
+
convert?: { svgToPdf(svgs: string[], options?: object): Promise<Uint8Array>; svgToPng(svg: string, options?: object): Promise<Uint8Array> };
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
onProgress?: (progress: ExportProgress) => void;
|
|
63
|
+
onDiagnostic?: (diagnostic: ExportDiagnostic) => void;
|
|
64
|
+
}
|
|
65
|
+
export interface ExportResult {
|
|
66
|
+
/** One PDF, PNG or SVG, or a ZIP of several. */
|
|
67
|
+
download: ExportFile;
|
|
68
|
+
files: ExportFile[];
|
|
69
|
+
diagnostics: ExportDiagnostic[];
|
|
70
|
+
/** The slide numbers (0-based) exported. */
|
|
71
|
+
slides: number[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Draw the deck with the renderer the preview uses and convert it. Rejects with an error whose `code` is `export-aborted`, `export-no-slides`,
|
|
75
|
+
* `export-format`, `export-unavailable` (PDF and PNG need the renderer's `export-browser` entry), `fonts-unavailable` or a renderer code.
|
|
76
|
+
*/
|
|
77
|
+
export declare function exportDeck(deck: unknown, options: ExportOptions): Promise<ExportResult>;
|