@openpresentation/opf-editor 0.12.1 → 0.15.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 +122 -80
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +16 -16
- package/dist/background-options.d.ts +12 -5
- package/dist/background-options.js +44 -19
- package/dist/block-controls.js +7 -7
- package/dist/block-convert.d.ts +7 -7
- package/dist/block-convert.js +22 -21
- package/dist/blocks.d.ts +8 -8
- package/dist/blocks.js +41 -40
- package/dist/canvas-fields.js +35 -12
- package/dist/canvas.d.ts +30 -31
- package/dist/canvas.js +64 -68
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- package/dist/chart-data.d.ts +10 -10
- package/dist/chart-data.js +21 -21
- package/dist/chart-options-panel.d.ts +2 -2
- package/dist/chart-options-panel.js +82 -8
- package/dist/chart-options.d.ts +35 -9
- package/dist/chart-options.js +133 -40
- package/dist/checks.js +23 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +17 -17
- package/dist/content-actions.js +74 -73
- package/dist/content-controls.js +21 -21
- package/dist/data-grid.js +7 -8
- package/dist/data.d.ts +4 -4
- package/dist/data.js +5 -5
- package/dist/dataset-refs.d.ts +2 -2
- package/dist/dataset-refs.js +5 -5
- package/dist/design-controls.d.ts +12 -5
- package/dist/design-controls.js +312 -111
- package/dist/design-options.d.ts +20 -18
- package/dist/design-options.js +91 -99
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +7 -7
- package/dist/export.js +48 -17
- package/dist/find-panel.js +5 -5
- package/dist/find-replace.d.ts +3 -3
- package/dist/find-replace.js +23 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +50 -101
- package/dist/grid-model.d.ts +25 -27
- package/dist/grid-model.js +92 -102
- package/dist/image-crop.d.ts +4 -4
- package/dist/image-crop.js +28 -29
- package/dist/image-cropper.js +3 -3
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +48 -31
- package/dist/index.js +154 -126
- package/dist/json-editor.js +2 -2
- package/dist/json-menu.js +2 -2
- package/dist/json-options.d.ts +3 -3
- package/dist/json-options.js +44 -54
- package/dist/layout-handles.js +5 -5
- package/dist/layout-placeholders.js +4 -4
- package/dist/layout.d.ts +2 -2
- package/dist/layout.js +8 -7
- package/dist/numbering-panel.js +3 -5
- package/dist/numbering.d.ts +3 -5
- package/dist/numbering.js +14 -26
- package/dist/outline-view.js +24 -24
- package/dist/outline.d.ts +12 -12
- package/dist/outline.js +52 -50
- package/dist/persistence.d.ts +1 -1
- package/dist/persistence.js +19 -18
- package/dist/presentation-ids.js +2 -2
- package/dist/react.js +3 -3
- package/dist/review-panel.d.ts +30 -13
- package/dist/review-panel.js +160 -53
- package/dist/review.d.ts +36 -43
- package/dist/review.js +92 -53
- package/dist/rich-text-input.js +4 -3
- package/dist/rich-text-toolbar.js +5 -3
- package/dist/rich-text.js +5 -2
- package/dist/schema-inspector.d.ts +2 -2
- package/dist/schema-inspector.js +16 -11
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +26 -6
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +19 -19
- package/dist/slides.d.ts +21 -21
- package/dist/slides.js +104 -101
- package/dist/svelte.js +2 -3
- package/dist/switches.d.ts +37 -22
- package/dist/switches.js +282 -171
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +28 -27
- package/dist/table-structure.d.ts +10 -10
- package/dist/table-structure.js +21 -21
- package/dist/template-panel.d.ts +3 -3
- package/dist/template-panel.js +37 -13
- package/dist/templates.d.ts +22 -11
- package/dist/templates.js +48 -55
- package/dist/transfer.d.ts +15 -4
- package/dist/transfer.js +107 -161
- package/package.json +12 -6
- package/dist/font-defaults.js +0 -25
package/dist/design-options.d.ts
CHANGED
|
@@ -7,15 +7,15 @@ export type DesignOptionId =
|
|
|
7
7
|
| "chartPrimary"
|
|
8
8
|
| "listBullet"
|
|
9
9
|
| "contentBox"
|
|
10
|
+
| "imageFit"
|
|
10
11
|
| "accentFont"
|
|
11
12
|
| "logo"
|
|
12
13
|
| "organizationLogo"
|
|
13
|
-
| "watermark"
|
|
14
|
-
| "slideImage";
|
|
14
|
+
| "watermark";
|
|
15
15
|
export interface DesignOptionDescriptor {
|
|
16
16
|
id: DesignOptionId;
|
|
17
17
|
label: string;
|
|
18
|
-
type: "enum" | "boolean" | "font" | "logo" | "organization-logo" | "watermark"
|
|
18
|
+
type: "enum" | "boolean" | "font" | "logo" | "organization-logo" | "watermark";
|
|
19
19
|
/** Allowed values of an enum option. */
|
|
20
20
|
values?: readonly string[];
|
|
21
21
|
scopes: ("deck" | "slide")[];
|
|
@@ -25,7 +25,7 @@ export declare const DESIGN_OPTIONS: readonly DesignOptionDescriptor[];
|
|
|
25
25
|
export declare const LOGO_VARIANTS: readonly ["default", "light", "dark", "stacked", "stackedLight", "stackedDark", "icon", "iconLight", "iconDark", "wordmark", "wordmarkLight", "wordmarkDark"];
|
|
26
26
|
export type LogoVariant = (typeof LOGO_VARIANTS)[number];
|
|
27
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"];
|
|
28
|
+
export declare const ZONE_FIELDS: readonly ["logo", "text", "image", "slideNumber", "slideNumberFormat", "date", "dateFormat", "organization", "speaker", "socials", "section"];
|
|
29
29
|
/** Date format tokens (English names). */
|
|
30
30
|
export declare const DATE_FORMAT_TOKENS: readonly string[];
|
|
31
31
|
export declare const HEADER_FOOTER_ZONES: readonly ["left", "center", "right"];
|
|
@@ -53,7 +53,7 @@ export interface PreparedDesignOption {
|
|
|
53
53
|
/** The logo variant or header/footer zone an edit named. */
|
|
54
54
|
variant?: string;
|
|
55
55
|
zone?: string;
|
|
56
|
-
|
|
56
|
+
presentation: unknown;
|
|
57
57
|
patches: JsonPatchOperation[];
|
|
58
58
|
changed: boolean;
|
|
59
59
|
/** Slides whose own design hides a deck-level change. */
|
|
@@ -61,8 +61,8 @@ export interface PreparedDesignOption {
|
|
|
61
61
|
/** Settings that need a logo the document does not have. */
|
|
62
62
|
warnings: DesignWarning[];
|
|
63
63
|
}
|
|
64
|
-
export interface DesignOptionChange extends Omit<EditorChange, "
|
|
65
|
-
|
|
64
|
+
export interface DesignOptionChange extends Omit<EditorChange, "presentation" | "patches"> {
|
|
65
|
+
presentation: unknown;
|
|
66
66
|
patches: JsonPatchOperation[];
|
|
67
67
|
option: string;
|
|
68
68
|
scope: "deck" | "slide";
|
|
@@ -72,17 +72,17 @@ export interface DesignOptionChange extends Omit<EditorChange, "document" | "pat
|
|
|
72
72
|
warnings: DesignWarning[];
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/** Compute the patch for one option. `null` removes it at that scope;
|
|
76
|
-
export declare function prepareDesignOption(
|
|
75
|
+
/** Compute the patch for one option. `null` removes it at that scope; the watermark object merges the fields passed. `imageFit` is the default fit of image blocks (cover, contain, stretch). */
|
|
76
|
+
export declare function prepareDesignOption(presentation: unknown, option: DesignOptionId, value: unknown, options?: DesignOptionOptions): PreparedDesignOption;
|
|
77
77
|
/** Set one option as a single undoable transaction. */
|
|
78
78
|
export declare function setDesignOption(editor: EditorSession, option: DesignOptionId, value: unknown, options?: DesignOptionOptions): DesignOptionChange;
|
|
79
79
|
/** `{ value, scope, inherited }`; scope is "slide", "deck" or "default". */
|
|
80
|
-
export declare function getDesignOption(
|
|
81
|
-
export declare function prepareLogoVariant(
|
|
80
|
+
export declare function getDesignOption(presentation: unknown, option: DesignOptionId, options?: Pick<DesignOptionOptions, "slideIndex" | "index">): { value: unknown; scope: "slide" | "deck" | "default"; inherited: boolean };
|
|
81
|
+
export declare function prepareLogoVariant(presentation: unknown, variant: LogoVariant, source: string | Record<string, unknown> | null, options?: DesignOptionOptions): PreparedDesignOption;
|
|
82
82
|
/** Set or clear one `design.logo` variant; a lone default stays a bare source. */
|
|
83
83
|
export declare function setLogoVariant(editor: EditorSession, variant: LogoVariant, source: string | Record<string, unknown> | null, options?: DesignOptionOptions): DesignOptionChange;
|
|
84
84
|
/** The variants `design.logo` sets at a scope (a bare logo is reported as `default`). */
|
|
85
|
-
export declare function readLogoVariants(
|
|
85
|
+
export declare function readLogoVariants(presentation: unknown, options?: Pick<DesignOptionOptions, "slideIndex">): Partial<Record<LogoVariant, unknown>>;
|
|
86
86
|
export interface HeaderFooterZoneFields {
|
|
87
87
|
logo?: boolean | null;
|
|
88
88
|
text?: string | null;
|
|
@@ -92,17 +92,19 @@ export interface HeaderFooterZoneFields {
|
|
|
92
92
|
date?: boolean | string | null;
|
|
93
93
|
dateFormat?: string | null;
|
|
94
94
|
organization?: boolean | null;
|
|
95
|
+
/** The first speaker's name and title ("Ada Lovelace, CTO"). */
|
|
96
|
+
speaker?: boolean | null;
|
|
95
97
|
socials?: boolean | null;
|
|
96
98
|
section?: boolean | null;
|
|
97
99
|
}
|
|
98
|
-
export declare function prepareHeaderFooterZone(
|
|
100
|
+
export declare function prepareHeaderFooterZone(presentation: unknown, which: "header" | "footer", zone: HeaderFooterZone, fields: HeaderFooterZoneFields, options?: DesignOptionOptions): PreparedDesignOption;
|
|
99
101
|
/** 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
102
|
export declare function setHeaderFooterZone(editor: EditorSession, which: "header" | "footer", zone: HeaderFooterZone, fields: HeaderFooterZoneFields, options?: DesignOptionOptions): DesignOptionChange;
|
|
101
103
|
/** 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(
|
|
104
|
+
export declare function readHeaderFooterZone(presentation: unknown, which: "header" | "footer", zone: HeaderFooterZone, options?: Pick<DesignOptionOptions, "slideIndex">): HeaderFooterZoneFields;
|
|
103
105
|
/** 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(
|
|
106
|
+
export declare function headerFooterState(presentation: unknown, which: "header" | "footer", options?: Pick<DesignOptionOptions, "slideIndex">): { own: boolean; inherited: boolean; hidden: boolean };
|
|
105
107
|
/** Whether a logo resolves for the slide: slide design, deck design, then the primary organization. */
|
|
106
|
-
export declare function hasResolvableLogo(
|
|
107
|
-
/** Settings that need content the document does not have: a logo (zones with `logo: true`, picture bullets),
|
|
108
|
-
export declare function designWarnings(
|
|
108
|
+
export declare function hasResolvableLogo(presentation: unknown, slideIndex: number): boolean;
|
|
109
|
+
/** Settings that need content the document does not have: a logo (zones with `logo: true`, picture bullets), an organization or its social profiles, or a named speaker, for zones that show them. */
|
|
110
|
+
export declare function designWarnings(presentation: unknown, slideIndex?: number): DesignWarning[];
|
package/dist/design-options.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// by default or on one slide with `slideIndex`, exactly like the dimension switches. Every
|
|
4
4
|
// function has a `prepare…` form that returns the patch without touching a session, and a
|
|
5
5
|
// session form that commits it with `meta.source: "design-option"`.
|
|
6
|
-
import { getValueAtPath, opfPathToJsonPointer
|
|
6
|
+
import { getValueAtPath, opfPathToJsonPointer } from "./index.js";
|
|
7
7
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
8
|
+
import { checkFormat } from "./checks.js";
|
|
8
9
|
|
|
9
10
|
/** The `design.logo` variant slots of a LogoSet, in schema order. */
|
|
10
11
|
export const LOGO_VARIANTS = Object.freeze([
|
|
@@ -23,8 +24,8 @@ export const LOGO_VARIANTS = Object.freeze([
|
|
|
23
24
|
]);
|
|
24
25
|
export const HEADER_FOOTER_ZONES = Object.freeze(["left", "center", "right"]);
|
|
25
26
|
// 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"]);
|
|
27
|
+
const ZONE_FLAGS = ["logo", "slideNumber", "organization", "speaker", "socials", "section"];
|
|
28
|
+
export const ZONE_FIELDS = Object.freeze(["logo", "text", "image", "slideNumber", "slideNumberFormat", "date", "dateFormat", "organization", "speaker", "socials", "section"]);
|
|
28
29
|
/** Date tokens a `dateFormat` understands (English names, independent of the host locale). */
|
|
29
30
|
export const DATE_FORMAT_TOKENS = Object.freeze(["yyyy", "yy", "MMMM", "MMM", "MM", "M", "dd", "d", "EEEE", "EEE"]);
|
|
30
31
|
const ISO_DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
@@ -32,7 +33,7 @@ const ISO_DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
|
32
33
|
// Friendly validation of one zone field before the schema sees it.
|
|
33
34
|
function checkZoneField(key, value) {
|
|
34
35
|
const bad = (message) => fail("invalid-design-value", message, { field: key, value });
|
|
35
|
-
if (
|
|
36
|
+
if (ZONE_FLAGS.includes(key) && typeof value !== "boolean") throw bad(`${key} is true or false.`);
|
|
36
37
|
if (key === "text" && typeof value !== "string") throw bad("Text is a string.");
|
|
37
38
|
if (key === "image" && typeof value !== "string" && !isObject(value)) throw bad("An image is a source, an asset reference or an asset object.");
|
|
38
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,19 +41,18 @@ function checkZoneField(key, value) {
|
|
|
40
41
|
if (key === "dateFormat" && (typeof value !== "string" || !value.trim())) throw bad(`A date format uses tokens such as ${DATE_FORMAT_TOKENS.join(", ")}.`);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const SLIDE_IMAGE_POSITIONS = ["background", "top", "bottom", "left", "right"];
|
|
44
|
-
|
|
45
44
|
const ENUMS = Object.freeze({
|
|
46
45
|
titleAlignment: ["left", "center", "right"],
|
|
47
46
|
contentAlignment: ["left", "center", "right"],
|
|
48
47
|
contentDirection: ["horizontal", "vertical"],
|
|
49
48
|
chartPrimary: ["none", "top", "bottom", "left", "right"],
|
|
50
49
|
listBullet: ["character", "image"],
|
|
50
|
+
imageFit: ["cover", "contain", "stretch"],
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Descriptors for every design option, in the order a panel shows them. `type` is "enum", "boolean",
|
|
55
|
-
* "font", "asset", "logo", "watermark"
|
|
55
|
+
* "font", "asset", "logo", "watermark" or "organization-logo"; `scopes` says whether
|
|
56
56
|
* the option applies to the deck, one slide, or both.
|
|
57
57
|
*/
|
|
58
58
|
export const DESIGN_OPTIONS = Object.freeze([
|
|
@@ -62,37 +62,42 @@ export const DESIGN_OPTIONS = Object.freeze([
|
|
|
62
62
|
{ id: "chartPrimary", label: "Primary chart position", type: "enum", values: ENUMS.chartPrimary, scopes: ["deck", "slide"], path: "design.chartPrimary" },
|
|
63
63
|
{ id: "listBullet", label: "List bullets", type: "enum", values: ENUMS.listBullet, scopes: ["deck", "slide"], path: "design.listBullet" },
|
|
64
64
|
{ id: "contentBox", label: "Content box", type: "boolean", scopes: ["deck", "slide"], path: "design.contentBox" },
|
|
65
|
-
{ id: "
|
|
65
|
+
{ id: "imageFit", label: "Image fit", type: "enum", values: ENUMS.imageFit, scopes: ["deck", "slide"], path: "design.imageFit" },
|
|
66
|
+
{ id: "accentFont", label: "Accent font", type: "font", scopes: ["deck", "slide"], path: "design.fontScheme.accent" },
|
|
66
67
|
{ id: "logo", label: "Logo", type: "logo", scopes: ["deck", "slide"], path: "design.logo" },
|
|
67
68
|
{ id: "organizationLogo", label: "Organization logo", type: "organization-logo", scopes: ["deck"], path: "organization.logo" },
|
|
68
69
|
{ 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
70
|
]);
|
|
71
71
|
const BY_ID = Object.fromEntries(DESIGN_OPTIONS.map((option) => [option.id, option]));
|
|
72
72
|
|
|
73
73
|
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
74
74
|
|
|
75
|
-
function scopeOf(
|
|
75
|
+
function scopeOf(presentation, option, options) {
|
|
76
76
|
const index = options.slideIndex;
|
|
77
77
|
if (index === undefined) return { scope: "deck", base: [] };
|
|
78
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) || !
|
|
79
|
+
if (!Number.isInteger(index) || !presentation.slides?.[index]) throw fail("slide-index-out-of-range", "Choose an existing slide for this option.", { slideIndex: index });
|
|
80
80
|
return { scope: "slide", base: ["slides", String(index)], slideIndex: index };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function ownDesign(
|
|
84
|
-
const design = getValueAtPath(
|
|
83
|
+
function ownDesign(presentation, base) {
|
|
84
|
+
const design = getValueAtPath(presentation, base.length ? [...base, "design"] : ["design"]);
|
|
85
85
|
return isObject(design) ? design : {};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function primaryOrganization(
|
|
89
|
-
const list = Array.isArray(
|
|
88
|
+
function primaryOrganization(presentation) {
|
|
89
|
+
const list = Array.isArray(presentation.organization) ? presentation.organization : presentation.organization ? [presentation.organization] : [];
|
|
90
90
|
return list.find((entry) => entry?.role === "primary") ?? list[0];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function firstSpeaker(presentation) {
|
|
94
|
+
const list = Array.isArray(presentation.speaker) ? presentation.speaker : presentation.speaker ? [presentation.speaker] : [];
|
|
95
|
+
return list[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
/** Whether a logo resolves for the slide: slide design, then deck design, then the primary organization. */
|
|
94
|
-
export function hasResolvableLogo(
|
|
95
|
-
return Boolean(
|
|
99
|
+
export function hasResolvableLogo(presentation, slideIndex) {
|
|
100
|
+
return Boolean(presentation.slides?.[slideIndex]?.design?.logo ?? presentation.design?.logo ?? primaryOrganization(presentation)?.logo);
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/**
|
|
@@ -100,21 +105,23 @@ export function hasResolvableLogo(document, slideIndex) {
|
|
|
100
105
|
* footer zone with `logo: true`, or picture bullets. The renderers fall back to a glyph or report
|
|
101
106
|
* unresolved content; the editor surfaces it before export.
|
|
102
107
|
*/
|
|
103
|
-
export function designWarnings(
|
|
104
|
-
const slide =
|
|
105
|
-
const design = { ...
|
|
108
|
+
export function designWarnings(presentation, slideIndex = 0) {
|
|
109
|
+
const slide = presentation.slides?.[slideIndex];
|
|
110
|
+
const design = { ...presentation.design, ...slide?.design };
|
|
106
111
|
const warnings = [];
|
|
107
|
-
const hasLogo = hasResolvableLogo(
|
|
112
|
+
const hasLogo = hasResolvableLogo(presentation, slideIndex);
|
|
108
113
|
for (const which of ["header", "footer"])
|
|
109
114
|
for (const zone of HEADER_FOOTER_ZONES)
|
|
110
115
|
if (!hasLogo && isObject(design[which]) && design[which][zone]?.logo === true)
|
|
111
116
|
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(
|
|
117
|
+
const organization = primaryOrganization(presentation);
|
|
113
118
|
for (const which of ["header", "footer"])
|
|
114
119
|
for (const zone of HEADER_FOOTER_ZONES) {
|
|
115
120
|
const item = isObject(design[which]) ? design[which][zone] : undefined;
|
|
116
121
|
if (item?.organization === true && !organization)
|
|
117
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.` });
|
|
118
125
|
if (item?.socials === true && !organization?.socials)
|
|
119
126
|
warnings.push({ code: "unresolved-content", path: `design.${which}.${zone}.socials`, message: `The ${which} ${zone} zone shows social profiles, but the organization has none.` });
|
|
120
127
|
}
|
|
@@ -123,17 +130,17 @@ export function designWarnings(document, slideIndex = 0) {
|
|
|
123
130
|
return warnings;
|
|
124
131
|
}
|
|
125
132
|
|
|
126
|
-
function shadowed(
|
|
127
|
-
return (
|
|
133
|
+
function shadowed(presentation, keys) {
|
|
134
|
+
return (presentation.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
function finish(
|
|
131
|
-
const before =
|
|
132
|
-
const next = checkedDocument(
|
|
137
|
+
function finish(presentation, patches, extra) {
|
|
138
|
+
const before = checkFormat(presentation);
|
|
139
|
+
const next = checkedDocument(presentation, patches, before);
|
|
133
140
|
const slideIndex = extra.slideIndex;
|
|
134
141
|
return {
|
|
135
142
|
...extra,
|
|
136
|
-
|
|
143
|
+
presentation: structuredClone(next),
|
|
137
144
|
patches,
|
|
138
145
|
changed: patches.length > 0,
|
|
139
146
|
warnings: designWarnings(next, slideIndex ?? 0),
|
|
@@ -145,49 +152,37 @@ function finish(document, patches, extra) {
|
|
|
145
152
|
function mergedWatermark(existing, value) {
|
|
146
153
|
if (value === null || value === false) return value;
|
|
147
154
|
if (typeof value === "string") return value;
|
|
148
|
-
if (!isObject(value)) throw fail("invalid-design-value", "A watermark is false, an image source or {
|
|
155
|
+
if (!isObject(value)) throw fail("invalid-design-value", "A watermark is false, an image source, { src, opacity } or { text, opacity }.", { value });
|
|
149
156
|
const base = typeof existing === "string" ? { src: existing } : isObject(existing) ? { ...existing } : {};
|
|
150
157
|
const merged = { ...base };
|
|
151
158
|
for (const [key, entry] of Object.entries(value)) {
|
|
152
159
|
if (entry === null || entry === undefined) delete merged[key];
|
|
153
160
|
else merged[key] = entry;
|
|
154
161
|
}
|
|
162
|
+
// FA-13: a watermark is an image (src) or a text stamp (text), never both: the field set last replaces the other.
|
|
163
|
+
if (value.text !== undefined && value.text !== null && value.src === undefined) delete merged.src;
|
|
164
|
+
else if (value.src !== undefined && value.src !== null && value.text === undefined) delete merged.text;
|
|
165
|
+
if (merged.text !== undefined && (typeof merged.text !== "string" || !merged.text.trim())) throw fail("invalid-design-value", "Watermark text is a non-empty string.", { value });
|
|
155
166
|
if (merged.opacity !== undefined && (typeof merged.opacity !== "number" || merged.opacity < 0 || merged.opacity > 1))
|
|
156
167
|
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 });
|
|
168
|
+
if (merged.src === undefined && merged.text === undefined && Object.keys(merged).length) throw fail("invalid-design-value", "Choose the watermark image before setting its opacity (or give it text instead).", { value });
|
|
158
169
|
if (Object.keys(merged).length === 1 && typeof merged.src === "string") return merged.src;
|
|
159
170
|
if (!Object.keys(merged).length) return null;
|
|
160
171
|
if (merged.opacity === undefined) throw fail("invalid-design-value", "Set an opacity from 0 to 1 for the watermark.", { value });
|
|
161
172
|
return merged;
|
|
162
173
|
}
|
|
163
174
|
|
|
164
|
-
function
|
|
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) {
|
|
175
|
+
function fontSchemeWithAccent(presentation, base, family) {
|
|
181
176
|
// The slide's own font scheme, else the deck's: an object form is kept with its overrides.
|
|
182
|
-
const own = ownDesign(
|
|
183
|
-
const inherited = base.length ?
|
|
177
|
+
const own = ownDesign(presentation, base).fontScheme;
|
|
178
|
+
const inherited = base.length ? presentation.design?.fontScheme : undefined;
|
|
184
179
|
const source = own !== undefined ? own : inherited;
|
|
185
180
|
const object = typeof source === "string" ? { id: source } : isObject(source) ? structuredClone(source) : {};
|
|
186
181
|
if (family === null) {
|
|
187
182
|
delete object.accent;
|
|
188
183
|
} else {
|
|
189
184
|
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 =
|
|
185
|
+
object.accent = family.trim();
|
|
191
186
|
}
|
|
192
187
|
const keys = Object.keys(object);
|
|
193
188
|
if (!keys.length) return null;
|
|
@@ -211,23 +206,23 @@ function collapseLogo(set) {
|
|
|
211
206
|
|
|
212
207
|
/**
|
|
213
208
|
* 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.
|
|
215
|
-
*
|
|
209
|
+
* removes the option at that scope so it is inherited again. The object-valued `watermark` merges the
|
|
210
|
+
* fields you pass into the existing object; a `null` field removes it.
|
|
216
211
|
* Options: `slideIndex` (one slide instead of the deck), `clearSlideOverrides` (deck scope: also
|
|
217
212
|
* remove slide-level values that hide it) and `index` (organizationLogo with several
|
|
218
213
|
* organizations).
|
|
219
214
|
*/
|
|
220
|
-
export function prepareDesignOption(
|
|
215
|
+
export function prepareDesignOption(presentation, option, value, options = {}) {
|
|
221
216
|
const descriptor = BY_ID[option];
|
|
222
217
|
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(
|
|
218
|
+
if (!isObject(presentation)) throw fail("invalid-input", "Design options need an OPF document object.");
|
|
224
219
|
if (value === undefined) throw fail("invalid-design-value", "Pass a value, or null to remove the option.", { option });
|
|
225
|
-
const { scope, base, slideIndex } = scopeOf(
|
|
220
|
+
const { scope, base, slideIndex } = scopeOf(presentation, option, options);
|
|
226
221
|
let patches = [];
|
|
227
222
|
let keys = [];
|
|
228
223
|
|
|
229
224
|
if (descriptor.type === "organization-logo") {
|
|
230
|
-
const organization =
|
|
225
|
+
const organization = presentation.organization;
|
|
231
226
|
const index = options.index ?? 0;
|
|
232
227
|
const owner = Array.isArray(organization) ? organization[index] : organization;
|
|
233
228
|
if (!isObject(owner)) throw fail("missing-owner", "Add an organization to the document before setting its logo.", { option });
|
|
@@ -245,38 +240,35 @@ export function prepareDesignOption(document, option, value, options = {}) {
|
|
|
245
240
|
if (value !== null && typeof value !== "boolean") throw fail("invalid-design-value", `${descriptor.label} is true or false.`, { option, value });
|
|
246
241
|
entries = { [option]: value };
|
|
247
242
|
} else if (descriptor.type === "font") {
|
|
248
|
-
entries = { fontScheme: fontSchemeWithAccent(
|
|
243
|
+
entries = { fontScheme: fontSchemeWithAccent(presentation, base, value) };
|
|
249
244
|
} else if (descriptor.type === "logo") {
|
|
250
245
|
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
246
|
entries = { logo: value };
|
|
252
|
-
} else if (descriptor.type === "watermark") {
|
|
253
|
-
const existing = ownDesign(document, base).watermark;
|
|
254
|
-
entries = { watermark: mergedWatermark(existing, value) };
|
|
255
247
|
} else {
|
|
256
|
-
const existing = ownDesign(
|
|
257
|
-
entries = {
|
|
248
|
+
const existing = ownDesign(presentation, base).watermark;
|
|
249
|
+
entries = { watermark: mergedWatermark(existing, value) };
|
|
258
250
|
}
|
|
259
251
|
keys = Object.keys(entries);
|
|
260
|
-
patches = designPatches(
|
|
252
|
+
patches = designPatches(presentation, base, entries);
|
|
261
253
|
}
|
|
262
254
|
|
|
263
255
|
let shadowedSlides = [];
|
|
264
256
|
if (scope === "deck" && keys.length) {
|
|
265
|
-
shadowedSlides = shadowed(
|
|
257
|
+
shadowedSlides = shadowed(presentation, keys);
|
|
266
258
|
if (options.clearSlideOverrides) {
|
|
267
259
|
for (const index of shadowedSlides)
|
|
268
260
|
for (const key of keys)
|
|
269
|
-
if (
|
|
261
|
+
if (presentation.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
|
|
270
262
|
shadowedSlides = [];
|
|
271
263
|
}
|
|
272
264
|
}
|
|
273
|
-
return finish(
|
|
265
|
+
return finish(presentation, patches, { option, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: shadowedSlides });
|
|
274
266
|
}
|
|
275
267
|
|
|
276
268
|
function apply(editor, prepared, meta = {}) {
|
|
277
|
-
const {
|
|
278
|
-
void
|
|
279
|
-
if (!prepared.changed) return { ...summary,
|
|
269
|
+
const { presentation, patches, ...summary } = prepared;
|
|
270
|
+
void presentation;
|
|
271
|
+
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
280
272
|
const change = editor.applyPatch(patches, { ...meta, source: meta.source ?? "design-option", option: prepared.option, scope: prepared.scope });
|
|
281
273
|
return { ...change, ...summary };
|
|
282
274
|
}
|
|
@@ -289,30 +281,30 @@ function checkEditor(editor) {
|
|
|
289
281
|
export function setDesignOption(editor, option, value, options = {}) {
|
|
290
282
|
checkEditor(editor);
|
|
291
283
|
const { meta, ...rest } = options;
|
|
292
|
-
return apply(editor, prepareDesignOption(editor.
|
|
284
|
+
return apply(editor, prepareDesignOption(editor.presentation, option, value, rest), meta);
|
|
293
285
|
}
|
|
294
286
|
|
|
295
287
|
/**
|
|
296
288
|
* Read one design option for a panel: `{ value, scope, inherited }` where `scope` is "slide" when the
|
|
297
289
|
* 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.
|
|
290
|
+
* `accentFont` reads the family name; `organizationLogo` reads the organization.
|
|
299
291
|
*/
|
|
300
|
-
export function getDesignOption(
|
|
292
|
+
export function getDesignOption(presentation, option, options = {}) {
|
|
301
293
|
const descriptor = BY_ID[option];
|
|
302
294
|
if (!descriptor) throw fail("unknown-design-option", `Unknown design option: ${option}.`, { option });
|
|
303
295
|
if (descriptor.type === "organization-logo") {
|
|
304
|
-
const organization =
|
|
296
|
+
const organization = presentation.organization;
|
|
305
297
|
const owner = Array.isArray(organization) ? organization[options.index ?? 0] : organization;
|
|
306
298
|
return { value: owner?.logo, scope: owner?.logo === undefined ? "default" : "deck", inherited: false };
|
|
307
299
|
}
|
|
308
300
|
const key = descriptor.type === "font" ? "fontScheme" : option;
|
|
309
301
|
const pick = (design) => {
|
|
310
302
|
const value = design?.[key];
|
|
311
|
-
return descriptor.type === "font" ? (isObject(value) ? value.accent
|
|
303
|
+
return descriptor.type === "font" ? (isObject(value) ? value.accent : undefined) : value;
|
|
312
304
|
};
|
|
313
|
-
const slideValue = options.slideIndex === undefined ? undefined : pick(
|
|
305
|
+
const slideValue = options.slideIndex === undefined ? undefined : pick(presentation.slides?.[options.slideIndex]?.design);
|
|
314
306
|
if (slideValue !== undefined) return { value: slideValue, scope: "slide", inherited: false };
|
|
315
|
-
const deckValue = pick(
|
|
307
|
+
const deckValue = pick(presentation.design);
|
|
316
308
|
if (deckValue !== undefined) return { value: deckValue, scope: "deck", inherited: options.slideIndex !== undefined };
|
|
317
309
|
return { value: undefined, scope: "default", inherited: false };
|
|
318
310
|
}
|
|
@@ -324,26 +316,26 @@ export function getDesignOption(document, option, options = {}) {
|
|
|
324
316
|
* variant of `design.logo`. A single default logo stays a bare source; adding a second variant turns
|
|
325
317
|
* it into a LogoSet, and clearing back to the default collapses it again.
|
|
326
318
|
*/
|
|
327
|
-
export function prepareLogoVariant(
|
|
319
|
+
export function prepareLogoVariant(presentation, variant, source, options = {}) {
|
|
328
320
|
if (!LOGO_VARIANTS.includes(variant)) throw fail("invalid-design-value", `Logo variant is one of ${LOGO_VARIANTS.join(", ")}.`, { variant });
|
|
329
321
|
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
322
|
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(
|
|
332
|
-
const set = logoSetOf(ownDesign(
|
|
323
|
+
const { scope, base, slideIndex } = scopeOf(presentation, "logo", options);
|
|
324
|
+
const set = logoSetOf(ownDesign(presentation, base).logo);
|
|
333
325
|
if (source === null) delete set[variant];
|
|
334
326
|
else set[variant] = typeof source === "string" ? source.trim() : source;
|
|
335
|
-
const patches = designPatches(
|
|
336
|
-
return finish(
|
|
327
|
+
const patches = designPatches(presentation, base, { logo: collapseLogo(set) });
|
|
328
|
+
return finish(presentation, patches, { option: "logo", variant, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
|
|
337
329
|
}
|
|
338
330
|
/** Set or clear one logo variant as a single undoable transaction. */
|
|
339
331
|
export function setLogoVariant(editor, variant, source, options = {}) {
|
|
340
332
|
checkEditor(editor);
|
|
341
333
|
const { meta, ...rest } = options;
|
|
342
|
-
return apply(editor, prepareLogoVariant(editor.
|
|
334
|
+
return apply(editor, prepareLogoVariant(editor.presentation, variant, source, rest), meta);
|
|
343
335
|
}
|
|
344
336
|
/** The variants `design.logo` sets at a scope: `{ variant: source }`, a bare logo reported as `default`. */
|
|
345
|
-
export function readLogoVariants(
|
|
346
|
-
const own = options.slideIndex === undefined ?
|
|
337
|
+
export function readLogoVariants(presentation, options = {}) {
|
|
338
|
+
const own = options.slideIndex === undefined ? presentation.design?.logo : presentation.slides?.[options.slideIndex]?.design?.logo;
|
|
347
339
|
return logoSetOf(own);
|
|
348
340
|
}
|
|
349
341
|
|
|
@@ -351,7 +343,7 @@ export function readLogoVariants(document, options = {}) {
|
|
|
351
343
|
|
|
352
344
|
/**
|
|
353
345
|
* 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`,
|
|
346
|
+
* image, slideNumber, slideNumberFormat, date, dateFormat, organization, speaker, socials, section); `null`,
|
|
355
347
|
* `false` for a flag, or an empty string removes a field. A zone left empty is removed, then an empty
|
|
356
348
|
* header or footer, so a slide never carries `{}`. A slide's own header or footer replaces the deck's
|
|
357
349
|
* whole one, so the first edit on a slide that has none of its own starts from a copy of the deck's
|
|
@@ -359,15 +351,15 @@ export function readLogoVariants(document, options = {}) {
|
|
|
359
351
|
* inheriting it again. Setting a field on a suppressed (`false`) header replaces the suppression.
|
|
360
352
|
* `logo: true` reports a warning when no logo resolves.
|
|
361
353
|
*/
|
|
362
|
-
export function prepareHeaderFooterZone(
|
|
354
|
+
export function prepareHeaderFooterZone(presentation, which, zone, fields, options = {}) {
|
|
363
355
|
if (!["header", "footer"].includes(which)) throw fail("invalid-design-value", "Choose header or footer.", { which });
|
|
364
356
|
if (!HEADER_FOOTER_ZONES.includes(zone)) throw fail("invalid-design-value", `Zone is one of ${HEADER_FOOTER_ZONES.join(", ")}.`, { zone });
|
|
365
357
|
if (!isObject(fields)) throw fail("invalid-design-value", "Pass the zone fields to change.", { fields });
|
|
366
358
|
const unknown = Object.keys(fields).filter((key) => !ZONE_FIELDS.includes(key));
|
|
367
359
|
if (unknown.length) throw fail("invalid-design-value", `Unknown header/footer field: ${unknown[0]}.`, { fields });
|
|
368
|
-
const { scope, base, slideIndex } = scopeOf(
|
|
369
|
-
const own = ownDesign(
|
|
370
|
-
const inherited = base.length ?
|
|
360
|
+
const { scope, base, slideIndex } = scopeOf(presentation, "logo", options);
|
|
361
|
+
const own = ownDesign(presentation, base)[which];
|
|
362
|
+
const inherited = base.length ? presentation.design?.[which] : undefined;
|
|
371
363
|
const current = own !== undefined ? own : inherited;
|
|
372
364
|
const container = isObject(current) ? structuredClone(current) : {};
|
|
373
365
|
const item = isObject(container[zone]) ? container[zone] : {};
|
|
@@ -386,27 +378,27 @@ export function prepareHeaderFooterZone(document, which, zone, fields, options =
|
|
|
386
378
|
let next = Object.keys(container).length ? container : null;
|
|
387
379
|
// Emptied: a deck value that would show through again is hidden explicitly; a suppressed header stays suppressed.
|
|
388
380
|
if (next === null && (own === false || isObject(inherited))) next = false;
|
|
389
|
-
const patches = designPatches(
|
|
390
|
-
return finish(
|
|
381
|
+
const patches = designPatches(presentation, base, { [which]: next });
|
|
382
|
+
return finish(presentation, patches, { option: which, zone, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
|
|
391
383
|
}
|
|
392
384
|
/** Edit one header or footer zone as a single undoable transaction. */
|
|
393
385
|
export function setHeaderFooterZone(editor, which, zone, fields, options = {}) {
|
|
394
386
|
checkEditor(editor);
|
|
395
387
|
const { meta, ...rest } = options;
|
|
396
|
-
return apply(editor, prepareHeaderFooterZone(editor.
|
|
388
|
+
return apply(editor, prepareHeaderFooterZone(editor.presentation, which, zone, fields, rest), meta);
|
|
397
389
|
}
|
|
398
390
|
/**
|
|
399
391
|
* One header or footer zone's fields as they apply at a scope: the slide's own header (or footer) when it
|
|
400
392
|
* has one, else the deck's (`{}` when absent or suppressed). `headerFooterState` says which.
|
|
401
393
|
*/
|
|
402
|
-
export function readHeaderFooterZone(
|
|
403
|
-
const own = options.slideIndex === undefined ? undefined :
|
|
404
|
-
const effective = own !== undefined ? own :
|
|
394
|
+
export function readHeaderFooterZone(presentation, which, zone, options = {}) {
|
|
395
|
+
const own = options.slideIndex === undefined ? undefined : presentation.slides?.[options.slideIndex]?.design?.[which];
|
|
396
|
+
const effective = own !== undefined ? own : presentation.design?.[which];
|
|
405
397
|
return isObject(effective) && isObject(effective[zone]) ? structuredClone(effective[zone]) : {};
|
|
406
398
|
}
|
|
407
399
|
/** `{ 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(
|
|
409
|
-
const own = options.slideIndex === undefined ?
|
|
410
|
-
const inherited = options.slideIndex !== undefined && own === undefined &&
|
|
411
|
-
return { own: own !== undefined, inherited, hidden: (own !== undefined ? own : options.slideIndex !== undefined ?
|
|
400
|
+
export function headerFooterState(presentation, which, options = {}) {
|
|
401
|
+
const own = options.slideIndex === undefined ? presentation.design?.[which] : presentation.slides?.[options.slideIndex]?.design?.[which];
|
|
402
|
+
const inherited = options.slideIndex !== undefined && own === undefined && presentation.design?.[which] !== undefined;
|
|
403
|
+
return { own: own !== undefined, inherited, hidden: (own !== undefined ? own : options.slideIndex !== undefined ? presentation.design?.[which] : undefined) === false };
|
|
412
404
|
}
|
package/dist/edit-helpers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Helpers shared by the dimension switches, the design options, the block conversions and the
|
|
2
2
|
// table options: the error constructor, structural equality and the design-key patch rules.
|
|
3
|
-
import { OPFEditorError, applyJsonPatch, getValueAtPath, opfPathToJsonPointer
|
|
3
|
+
import { OPFEditorError, applyJsonPatch, getValueAtPath, opfPathToJsonPointer } from "./index.js";
|
|
4
|
+
import { firstErrorMessage, errorFindings, checkFormat } from "./checks.js";
|
|
4
5
|
|
|
5
6
|
export function fail(code, message, details) {
|
|
6
7
|
return new OPFEditorError(code, message, details);
|
|
@@ -19,8 +20,8 @@ export function same(a, b) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
// Set (value) or remove (null) design keys at deck or slide scope.
|
|
22
|
-
export function designPatches(
|
|
23
|
-
const design = getValueAtPath(
|
|
23
|
+
export function designPatches(presentation, base, entries) {
|
|
24
|
+
const design = getValueAtPath(presentation, base.length ? [...base, "design"] : ["design"]);
|
|
24
25
|
const at = (key) => opfPathToJsonPointer([...base, "design", key]);
|
|
25
26
|
const set = Object.entries(entries).filter(([, value]) => value !== undefined && value !== null);
|
|
26
27
|
if (!design || typeof design !== "object" || Array.isArray(design))
|
|
@@ -41,12 +42,12 @@ export function designPatches(document, base, entries) {
|
|
|
41
42
|
* Validate a candidate patch the way every switch does: the result must be valid OPF unless the
|
|
42
43
|
* input document was already invalid (then nothing new may be reported as the cause).
|
|
43
44
|
*/
|
|
44
|
-
export function checkedDocument(
|
|
45
|
-
const next = patches.length ? applyJsonPatch(
|
|
45
|
+
export function checkedDocument(presentation, patches, before) {
|
|
46
|
+
const next = patches.length ? applyJsonPatch(presentation, patches) : presentation;
|
|
46
47
|
if (patches.length) {
|
|
47
|
-
const validation =
|
|
48
|
+
const validation = checkFormat(next);
|
|
48
49
|
if (!validation.valid && before.valid)
|
|
49
|
-
throw fail("invalid-opf-edit", validation
|
|
50
|
+
throw fail("invalid-opf-edit", firstErrorMessage(validation, "This change produces an invalid document."), { issues: errorFindings(validation), patches });
|
|
50
51
|
}
|
|
51
52
|
return next;
|
|
52
53
|
}
|
package/dist/exact-source.js
CHANGED
|
@@ -228,11 +228,11 @@ function typingSignature(previous, ops) {
|
|
|
228
228
|
/**
|
|
229
229
|
* Change only the affected values. Keeps authored indentation, escaping and all unrelated
|
|
230
230
|
* metadata/content byte-for-byte, including across canvas Escape, Undo and Redo.
|
|
231
|
-
* Throws when `source` is not valid JSON. The result always parses to `
|
|
231
|
+
* Throws when `source` is not valid JSON. The result always parses to `presentation`; if an
|
|
232
232
|
* in-place edit cannot achieve that (for example the source repeats an object key), the whole
|
|
233
233
|
* document is written as indented JSON instead.
|
|
234
234
|
*/
|
|
235
|
-
export function updateJsonSource(source,
|
|
235
|
+
export function updateJsonSource(source, presentation, memory = sharedMemory) {
|
|
236
236
|
const previous = JSON.parse(source);
|
|
237
237
|
const previousKey = JSON.stringify(previous);
|
|
238
238
|
memory.limited = source.length > MAX_EXACT_SOURCE_LENGTH;
|
|
@@ -243,9 +243,9 @@ export function updateJsonSource(source, document, memory = sharedMemory) {
|
|
|
243
243
|
memory.last = null;
|
|
244
244
|
}
|
|
245
245
|
const ops = [];
|
|
246
|
-
diff(previous,
|
|
246
|
+
diff(previous, presentation, [], ops);
|
|
247
247
|
if (!ops.length) return source;
|
|
248
|
-
const key = JSON.stringify(
|
|
248
|
+
const key = JSON.stringify(presentation);
|
|
249
249
|
const remembered = recall(memory, key);
|
|
250
250
|
if (remembered !== undefined && equalOutside(source, remembered, maskPaths(ops))) {
|
|
251
251
|
memory.last = null;
|
|
@@ -306,7 +306,7 @@ export function updateJsonSource(source, document, memory = sharedMemory) {
|
|
|
306
306
|
valid = JSON.stringify(parsed) === key || canonical(parsed) === canonical(JSON.parse(key));
|
|
307
307
|
} catch { /* Fall back below. */ }
|
|
308
308
|
if (!valid) {
|
|
309
|
-
result = JSON.stringify(
|
|
309
|
+
result = JSON.stringify(presentation, null, indent).replaceAll("\n", eol);
|
|
310
310
|
if (/\n$/.test(source)) result += eol;
|
|
311
311
|
}
|
|
312
312
|
|