@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.
Files changed (103) hide show
  1. package/README.md +122 -80
  2. package/dist/annotations.d.ts +16 -16
  3. package/dist/annotations.js +71 -79
  4. package/dist/assets.d.ts +7 -7
  5. package/dist/assets.js +16 -16
  6. package/dist/background-options.d.ts +12 -5
  7. package/dist/background-options.js +44 -19
  8. package/dist/block-controls.js +7 -7
  9. package/dist/block-convert.d.ts +7 -7
  10. package/dist/block-convert.js +22 -21
  11. package/dist/blocks.d.ts +8 -8
  12. package/dist/blocks.js +41 -40
  13. package/dist/canvas-fields.js +35 -12
  14. package/dist/canvas.d.ts +30 -31
  15. package/dist/canvas.js +64 -68
  16. package/dist/catalogs.d.ts +48 -0
  17. package/dist/catalogs.js +217 -0
  18. package/dist/chart-data.d.ts +10 -10
  19. package/dist/chart-data.js +21 -21
  20. package/dist/chart-options-panel.d.ts +2 -2
  21. package/dist/chart-options-panel.js +82 -8
  22. package/dist/chart-options.d.ts +35 -9
  23. package/dist/chart-options.js +133 -40
  24. package/dist/checks.js +23 -0
  25. package/dist/color-authoring.js +1 -1
  26. package/dist/content-actions.d.ts +17 -17
  27. package/dist/content-actions.js +74 -73
  28. package/dist/content-controls.js +21 -21
  29. package/dist/data-grid.js +7 -8
  30. package/dist/data.d.ts +4 -4
  31. package/dist/data.js +5 -5
  32. package/dist/dataset-refs.d.ts +2 -2
  33. package/dist/dataset-refs.js +5 -5
  34. package/dist/design-controls.d.ts +12 -5
  35. package/dist/design-controls.js +312 -111
  36. package/dist/design-options.d.ts +20 -18
  37. package/dist/design-options.js +91 -99
  38. package/dist/edit-helpers.js +8 -7
  39. package/dist/exact-source.js +5 -5
  40. package/dist/export.d.ts +7 -7
  41. package/dist/export.js +48 -17
  42. package/dist/find-panel.js +5 -5
  43. package/dist/find-replace.d.ts +3 -3
  44. package/dist/find-replace.js +23 -20
  45. package/dist/font-gate.js +49 -58
  46. package/dist/galleries.js +50 -101
  47. package/dist/grid-model.d.ts +25 -27
  48. package/dist/grid-model.js +92 -102
  49. package/dist/image-crop.d.ts +4 -4
  50. package/dist/image-crop.js +28 -29
  51. package/dist/image-cropper.js +3 -3
  52. package/dist/image-options.d.ts +45 -0
  53. package/dist/image-options.js +195 -0
  54. package/dist/index.d.ts +48 -31
  55. package/dist/index.js +154 -126
  56. package/dist/json-editor.js +2 -2
  57. package/dist/json-menu.js +2 -2
  58. package/dist/json-options.d.ts +3 -3
  59. package/dist/json-options.js +44 -54
  60. package/dist/layout-handles.js +5 -5
  61. package/dist/layout-placeholders.js +4 -4
  62. package/dist/layout.d.ts +2 -2
  63. package/dist/layout.js +8 -7
  64. package/dist/numbering-panel.js +3 -5
  65. package/dist/numbering.d.ts +3 -5
  66. package/dist/numbering.js +14 -26
  67. package/dist/outline-view.js +24 -24
  68. package/dist/outline.d.ts +12 -12
  69. package/dist/outline.js +52 -50
  70. package/dist/persistence.d.ts +1 -1
  71. package/dist/persistence.js +19 -18
  72. package/dist/presentation-ids.js +2 -2
  73. package/dist/react.js +3 -3
  74. package/dist/review-panel.d.ts +30 -13
  75. package/dist/review-panel.js +160 -53
  76. package/dist/review.d.ts +36 -43
  77. package/dist/review.js +92 -53
  78. package/dist/rich-text-input.js +4 -3
  79. package/dist/rich-text-toolbar.js +5 -3
  80. package/dist/rich-text.js +5 -2
  81. package/dist/schema-inspector.d.ts +2 -2
  82. package/dist/schema-inspector.js +16 -11
  83. package/dist/schema.d.ts +1 -1
  84. package/dist/schema.js +26 -6
  85. package/dist/slide-manager.d.ts +2 -2
  86. package/dist/slide-manager.js +19 -19
  87. package/dist/slides.d.ts +21 -21
  88. package/dist/slides.js +104 -101
  89. package/dist/svelte.js +2 -3
  90. package/dist/switches.d.ts +37 -22
  91. package/dist/switches.js +282 -171
  92. package/dist/table-options.d.ts +8 -8
  93. package/dist/table-options.js +28 -27
  94. package/dist/table-structure.d.ts +10 -10
  95. package/dist/table-structure.js +21 -21
  96. package/dist/template-panel.d.ts +3 -3
  97. package/dist/template-panel.js +37 -13
  98. package/dist/templates.d.ts +22 -11
  99. package/dist/templates.js +48 -55
  100. package/dist/transfer.d.ts +15 -4
  101. package/dist/transfer.js +107 -161
  102. package/package.json +12 -6
  103. package/dist/font-defaults.js +0 -25
@@ -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" | "slide-image";
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
- document: unknown;
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, "document" | "patches"> {
65
- document: unknown;
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; object options (watermark, slideImage) merge the fields passed. */
76
- export declare function prepareDesignOption(document: unknown, option: DesignOptionId, value: unknown, options?: DesignOptionOptions): PreparedDesignOption;
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(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;
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(document: unknown, options?: Pick<DesignOptionOptions, "slideIndex">): Partial<Record<LogoVariant, unknown>>;
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(document: unknown, which: "header" | "footer", zone: HeaderFooterZone, fields: HeaderFooterZoneFields, options?: DesignOptionOptions): PreparedDesignOption;
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(document: unknown, which: "header" | "footer", zone: HeaderFooterZone, options?: Pick<DesignOptionOptions, "slideIndex">): HeaderFooterZoneFields;
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(document: unknown, which: "header" | "footer", options?: Pick<DesignOptionOptions, "slideIndex">): { own: boolean; inherited: boolean; hidden: boolean };
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(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[];
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[];
@@ -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, validateOpfDocument } from "./index.js";
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 (["logo", "slideNumber", "organization", "socials", "section"].includes(key) && typeof value !== "boolean") throw bad(`${key} is true or false.`);
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", "slide-image" or "organization-logo"; `scopes` says whether
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: "accentFont", label: "Accent font", type: "font", scopes: ["deck", "slide"], path: "design.fontScheme.accent.family" },
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(document, option, options) {
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) || !document.slides?.[index]) throw fail("slide-index-out-of-range", "Choose an existing slide for this option.", { slideIndex: 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(document, base) {
84
- const design = getValueAtPath(document, base.length ? [...base, "design"] : ["design"]);
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(document) {
89
- const list = Array.isArray(document.organization) ? document.organization : document.organization ? [document.organization] : [];
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(document, slideIndex) {
95
- return Boolean(document.slides?.[slideIndex]?.design?.logo ?? document.design?.logo ?? primaryOrganization(document)?.logo);
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(document, slideIndex = 0) {
104
- const slide = document.slides?.[slideIndex];
105
- const design = { ...document.design, ...slide?.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(document, slideIndex);
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(document);
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(document, keys) {
127
- return (document.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
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(document, patches, extra) {
131
- const before = validateOpfDocument(document);
132
- const next = checkedDocument(document, patches, before);
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
- document: structuredClone(next),
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 { src, opacity }.", { value });
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 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) {
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(document, base).fontScheme;
183
- const inherited = base.length ? document.design?.fontScheme : undefined;
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 = { ...(isObject(object.accent) ? object.accent : {}), family: family.trim() };
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. Object-valued options (`watermark`,
215
- * `slideImage`) merge the fields you pass into the existing object; a `null` field removes it.
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(document, option, value, options = {}) {
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(document)) throw fail("invalid-input", "Design options need an OPF document object.");
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(document, option, options);
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 = document.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(document, base, value) };
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(document, base).slideImage;
257
- entries = { slideImage: mergedSlideImage(existing, value) };
248
+ const existing = ownDesign(presentation, base).watermark;
249
+ entries = { watermark: mergedWatermark(existing, value) };
258
250
  }
259
251
  keys = Object.keys(entries);
260
- patches = designPatches(document, base, entries);
252
+ patches = designPatches(presentation, base, entries);
261
253
  }
262
254
 
263
255
  let shadowedSlides = [];
264
256
  if (scope === "deck" && keys.length) {
265
- shadowedSlides = shadowed(document, keys);
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 (document.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
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(document, patches, { option, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: shadowedSlides });
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 { document, patches, ...summary } = prepared;
278
- void document;
279
- if (!prepared.changed) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
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.document, option, value, rest), meta);
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(document, option, options = {}) {
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 = document.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?.family : undefined) : value;
303
+ return descriptor.type === "font" ? (isObject(value) ? value.accent : undefined) : value;
312
304
  };
313
- const slideValue = options.slideIndex === undefined ? undefined : pick(document.slides?.[options.slideIndex]?.design);
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(document.design);
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(document, variant, source, options = {}) {
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(document, "logo", options);
332
- const set = logoSetOf(ownDesign(document, base).logo);
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(document, base, { logo: collapseLogo(set) });
336
- return finish(document, patches, { option: "logo", variant, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
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.document, variant, source, rest), meta);
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(document, options = {}) {
346
- const own = options.slideIndex === undefined ? document.design?.logo : document.slides?.[options.slideIndex]?.design?.logo;
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(document, which, zone, fields, options = {}) {
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(document, "logo", options);
369
- const own = ownDesign(document, base)[which];
370
- const inherited = base.length ? document.design?.[which] : undefined;
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(document, base, { [which]: next });
390
- return finish(document, patches, { option: which, zone, scope, ...(slideIndex !== undefined ? { slideIndex } : {}), shadowed: [] });
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.document, which, zone, fields, rest), meta);
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(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];
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(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 };
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
  }
@@ -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, validateOpfDocument } from "./index.js";
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(document, base, entries) {
23
- const design = getValueAtPath(document, base.length ? [...base, "design"] : ["design"]);
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(document, patches, before) {
45
- const next = patches.length ? applyJsonPatch(document, patches) : document;
45
+ export function checkedDocument(presentation, patches, before) {
46
+ const next = patches.length ? applyJsonPatch(presentation, patches) : presentation;
46
47
  if (patches.length) {
47
- const validation = validateOpfDocument(next);
48
+ const validation = checkFormat(next);
48
49
  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
+ 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
  }
@@ -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 `document`; if an
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, document, memory = sharedMemory) {
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, document, [], ops);
246
+ diff(previous, presentation, [], ops);
247
247
  if (!ops.length) return source;
248
- const key = JSON.stringify(document);
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(document, null, indent).replaceAll("\n", eol);
309
+ result = JSON.stringify(presentation, null, indent).replaceAll("\n", eol);
310
310
  if (/\n$/.test(source)) result += eol;
311
311
  }
312
312