@openpresentation/opf-editor 0.7.1 → 0.9.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/dist/galleries.js CHANGED
@@ -117,6 +117,53 @@ export async function loadOpfGallery(input, options = {}) {
117
117
  })),
118
118
  };
119
119
  }
120
+ const FONT_SCHEME_ENUMS = {
121
+ type: ["sans-serif", "serif", "monospace"],
122
+ app: ["PowerPoint", "Google Slides"],
123
+ languageFamily: ["latin", "ea", "cs"],
124
+ };
125
+ // A Font role object ({ family, weight?, style?, letterSpacing? }), or undefined.
126
+ function fontRole(value) {
127
+ if (typeof value === "string") return value ? { family: value } : undefined;
128
+ if (!value || typeof value !== "object" || typeof value.family !== "string")
129
+ return undefined;
130
+ const role = { family: value.family };
131
+ if (typeof value.weight === "number" && Number.isFinite(value.weight))
132
+ role.weight = value.weight;
133
+ if (value.style === "normal" || value.style === "italic")
134
+ role.style = value.style;
135
+ if (
136
+ typeof value.letterSpacing === "number" &&
137
+ Number.isFinite(value.letterSpacing)
138
+ )
139
+ role.letterSpacing = value.letterSpacing;
140
+ return role;
141
+ }
142
+ // Keep every font-scheme role that the catalog record schema defines: the OOXML
143
+ // pair and `code`. Gallery `heading`/`body` (string or Font object) map onto
144
+ // major/minor only, never role objects, so a later inline design.fontScheme
145
+ // major/minor override still wins (resolveFontFamilies checks roles before the
146
+ // pair). `accent` is not part of the record schema and is dropped.
147
+ function fontSchemeRecord(source, id) {
148
+ const record = {
149
+ $schema: "https://openpresentation.org/schema/opf-font-scheme/v1",
150
+ id,
151
+ name: source.name ?? id,
152
+ major:
153
+ typeof source.major === "string"
154
+ ? source.major
155
+ : fontRole(source.heading)?.family,
156
+ minor:
157
+ typeof source.minor === "string"
158
+ ? source.minor
159
+ : fontRole(source.body)?.family,
160
+ };
161
+ for (const [field, values] of Object.entries(FONT_SCHEME_ENUMS))
162
+ if (values.includes(source[field])) record[field] = source[field];
163
+ const code = fontRole(source.code);
164
+ if (code) record.code = code;
165
+ return record;
166
+ }
120
167
  function attachDefinition(document, descriptor) {
121
168
  const source = descriptor.metadata?.source,
122
169
  category = descriptor.category ?? descriptor.metadata?.category;
@@ -161,14 +208,7 @@ function attachDefinition(document, descriptor) {
161
208
  }
162
209
  : {}),
163
210
  };
164
- } else if (kind === "fontSchemes")
165
- record = {
166
- $schema: "https://openpresentation.org/schema/opf-font-scheme/v1",
167
- id,
168
- name: source.name ?? id,
169
- major: source.major ?? source.heading,
170
- minor: source.minor ?? source.body,
171
- };
211
+ } else if (kind === "fontSchemes") record = fontSchemeRecord(source, id);
172
212
  else {
173
213
  record = {
174
214
  ...source,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { PaginationOptions, PaginationResult } from "@openpresentation/opf/pagination";
2
2
  import type { Composition, ComposeSlideOptions, SlideComposition } from "@openpresentation/opf/composition";
3
+ /** Reported when a font-scheme id matches no inline or bundled record; the default font scheme (`aptos`) is the base. */
4
+ export interface FontSchemeDiagnostic { code: "unresolved-font-scheme"; path: string; message: string; id: string; fallback: string }
5
+ export interface EditorDiagnosticOptions { onDiagnostic?: (diagnostic: FontSchemeDiagnostic) => void }
3
6
  export declare const packageName = "@openpresentation/opf-editor";
4
7
 
5
8
  export declare const releaseLane: Readonly<{
@@ -26,6 +29,7 @@ export type JsonPatchOperation =
26
29
  export interface OPFValidationSummary {
27
30
  valid: boolean;
28
31
  errors: unknown[];
32
+ warnings: unknown[];
29
33
  result: unknown;
30
34
  }
31
35
 
@@ -57,8 +61,8 @@ export interface EditorEvent {
57
61
  }
58
62
 
59
63
  export interface EditorSession {
60
- paginateSlide(slideIndex: number, options?: PaginationOptions, meta?: Record<string, unknown>): { change: EditorChange | null; pagination: PaginationResult };
61
- composeSlide(slideIndex: number, options?: ComposeSlideOptions): SlideComposition;
64
+ paginateSlide(slideIndex: number, options?: PaginationOptions & EditorDiagnosticOptions, meta?: Record<string, unknown>): { change: EditorChange | null; pagination: PaginationResult };
65
+ composeSlide(slideIndex: number, options?: ComposeSlideOptions & EditorDiagnosticOptions): SlideComposition;
62
66
  setComposition(slideIndex: number, composition: Composition, meta?: Record<string, unknown>): EditorChange;
63
67
  readonly document: unknown;
64
68
  readonly validation: OPFValidationSummary;
@@ -158,6 +162,9 @@ export declare function invertJsonPatch(document: unknown, operations: JsonPatch
158
162
 
159
163
  export declare function validateOpfDocument(document: unknown, validator?: (document: unknown) => unknown): OPFValidationSummary;
160
164
 
165
+ /** Font families the slide resolves to (slide, deck, theme, then the shared default scheme). */
166
+ export declare function resolveSlideFonts(document: unknown, slideIndex?: number, options?: EditorDiagnosticOptions): { heading?: string; body?: string; code?: string; [role: string]: string | undefined };
167
+
161
168
  export declare function createEditorSession(input: unknown, options?: CreateEditorSessionOptions): EditorSession;
162
169
 
163
170
  export declare function createSvgTraceBinding(
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { paginateSlide } from "@openpresentation/opf/pagination";
2
+ import { collectReservedPresentationIds } from "./presentation-ids.js";
3
+ import { DEFAULT_FONT_SCHEME, resolveFontSchemeReference } from "./font-defaults.js";
2
4
  import { composeSlide, resolveCanvasDimensions, resolveFontFamilies } from "@openpresentation/opf/composition";
3
5
  import {
4
6
  catalogKinds,
@@ -6,7 +8,7 @@ import {
6
8
  validatePresentation
7
9
  } from "@openpresentation/opf";
8
10
 
9
- function resolveCompositionOptions(document, slideIndex, options = {}) {
11
+ function resolveCompositionOptions(document, slideIndex, { onDiagnostic, ...options } = {}) {
10
12
  const slide = document.slides?.[slideIndex];
11
13
  if (!Number.isInteger(slideIndex) || !slide) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
12
14
  const inline = document.catalogs?.layouts?.records ?? [];
@@ -16,12 +18,22 @@ function resolveCompositionOptions(document, slideIndex, options = {}) {
16
18
  const themeId = typeof themeReference === "string" ? themeReference : themeReference.id;
17
19
  const theme = document.catalogs?.themes?.records?.find(record => record.id === themeId)
18
20
  ?? bundledCatalogs.themes.find(record => record.id === themeId);
19
- const reference = slide.design?.fontScheme ?? document.design?.fontScheme ?? theme?.fontScheme ?? "roboto";
20
- const id = typeof reference === "string" ? reference : reference.id;
21
- const fontScheme = {...(document.catalogs?.fontSchemes?.records?.find(record=>record.id===id) ?? bundledCatalogs.fontSchemes.find(record=>record.id===id)),...(typeof reference === "object" ? reference : {})};
21
+ const reference = slide.design?.fontScheme ?? document.design?.fontScheme ?? theme?.fontScheme ?? DEFAULT_FONT_SCHEME;
22
+ const fontPath = slide.design?.fontScheme !== undefined ? `slides.${slideIndex}.design.fontScheme` : document.design?.fontScheme !== undefined ? "design.fontScheme" : slide.design?.theme !== undefined ? `slides.${slideIndex}.design.theme` : "design.theme";
23
+ const { scheme: fontScheme, diagnostic } = resolveFontSchemeReference(reference, id => document.catalogs?.fontSchemes?.records?.find(record=>record.id===id) ?? bundledCatalogs.fontSchemes.find(record=>record.id===id), fontPath);
24
+ if (diagnostic) onDiagnostic?.(diagnostic);
22
25
  return { ...resolveCanvasDimensions(slide.design?.dimensions ?? document.design?.dimensions ?? theme?.dimensions), fonts:resolveFontFamilies(fontScheme), contentAlignment:slide.design?.contentAlignment??document.design?.contentAlignment, titleAlignment:slide.design?.titleAlignment??document.design?.titleAlignment, contentBox:slide.design?.contentBox??document.design?.contentBox, presentation:document, ...options, layout, slideIndex };
23
26
  }
24
27
 
28
+ /**
29
+ * The font families a slide resolves to (`heading`, `body`, `code`), by the same slide, deck,
30
+ * theme, default order that composition, pagination and export use. Hosts read this after a
31
+ * font-scheme or theme switch to show the fonts the preview now uses.
32
+ */
33
+ export function resolveSlideFonts(document, slideIndex = 0, options = {}) {
34
+ return resolveCompositionOptions(document, slideIndex, options).fonts;
35
+ }
36
+
25
37
  export const packageName = "@openpresentation/opf-editor";
26
38
 
27
39
  export const releaseLane = Object.freeze({
@@ -142,6 +154,7 @@ export function validateOpfDocument(document, validator = validatePresentation)
142
154
  return {
143
155
  valid,
144
156
  errors: Array.isArray(result?.errors) ? result.errors : [],
157
+ warnings: Array.isArray(result?.warnings) ? result.warnings : [],
145
158
  result
146
159
  };
147
160
  }
@@ -243,7 +256,10 @@ export function createEditorSession(input, options = {}) {
243
256
  },
244
257
  paginateSlide(slideIndex, options = {}, meta = {}) {
245
258
  const resolved = resolveCompositionOptions(document, slideIndex, options);
246
- const pagination = paginateSlide(document.slides[slideIndex], { ...resolved, reservedIds: document.slides.map(slide => slide.id).filter(Boolean) });
259
+ const pagination = paginateSlide(document.slides[slideIndex], {
260
+ ...resolved,
261
+ reservedIds: collectReservedPresentationIds(document),
262
+ });
247
263
  // Pagination can persist a readability policy without adding a page. Commit
248
264
  // that change too, so preview/export use the same minimum that was evaluated.
249
265
  if (pagination.slides.length === 1 && jsonEqual(pagination.slides[0], document.slides[slideIndex])) return { change: null, pagination };
package/dist/layout.d.ts CHANGED
@@ -5,6 +5,6 @@ export declare function prepareTrackResize(document:unknown,flow:ComposedFlow,bo
5
5
  document:unknown;patches:JsonPatchOperation[];composition:Composition;fraction:number;
6
6
  };
7
7
 
8
- export {prepareBlockMove,listBlockContainers,prepareBlockInsert,prepareBlockDuplicate,prepareBlockRemove,createContentBlock} from './blocks.js';
8
+ export {prepareBlockMove,listBlockContainers,prepareBlockInsert,prepareBlockDuplicate,prepareBlockRemove,prepareBlockReplace,createContentBlock} from './blocks.js';
9
9
 
10
10
  export type {PreparedBlockChange,ContentBlockKind} from './blocks.js';
package/dist/layout.js CHANGED
@@ -31,4 +31,4 @@ export function prepareTrackResize(document, flow, boundary, fraction) {
31
31
  return {document:next,patches,composition,fraction:share};
32
32
  }
33
33
 
34
- export {prepareBlockMove,listBlockContainers,prepareBlockInsert,prepareBlockDuplicate,prepareBlockRemove,createContentBlock} from './blocks.js';
34
+ export {prepareBlockMove,listBlockContainers,prepareBlockInsert,prepareBlockDuplicate,prepareBlockRemove,prepareBlockReplace,createContentBlock} from './blocks.js';
@@ -0,0 +1,68 @@
1
+ /** Shared slide and content-payload id collection (matches @openpresentation/opf pagination). */
2
+
3
+ const promotedRegionKeys = [
4
+ "left", "center", "right", "left+center", "center+right", "left+center+right",
5
+ "top", "middle", "bottom", "top+middle", "middle+bottom", "top+middle+bottom",
6
+ "top:left", "top:center", "top:right", "top:left+center", "top:center+right", "top:left+center+right",
7
+ "middle:left", "middle:center", "middle:right", "middle:left+center", "middle:center+right", "middle:left+center+right",
8
+ "bottom:left", "bottom:center", "bottom:right", "bottom:left+center", "bottom:center+right", "bottom:left+center+right",
9
+ "top+middle:left", "top+middle:center", "top+middle:right", "top+middle:left+center", "top+middle:center+right", "top+middle:left+center+right",
10
+ "middle+bottom:left", "middle+bottom:center", "middle+bottom:right", "middle+bottom:left+center", "middle+bottom:center+right", "middle+bottom:left+center+right",
11
+ "top+middle+bottom:left", "top+middle+bottom:center", "top+middle+bottom:right", "top+middle+bottom:left+center", "top+middle+bottom:center+right", "top+middle+bottom:left+center+right",
12
+ ];
13
+
14
+ const MAX_COMPOSITION_DEPTH = 8;
15
+
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+
20
+ export function visitContentPayloads(slide, slidePath, visit) {
21
+ const walk = (payload, path, depth, ancestors) => {
22
+ visit(payload, path);
23
+ if (!Array.isArray(payload.blocks) || depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(payload)) return;
24
+ const nested = [...ancestors, payload];
25
+ payload.blocks.forEach((block, index) => {
26
+ if (isRecord(block)) walk(block, path ? `${path}.blocks.${index}` : `blocks.${index}`, depth + 1, nested);
27
+ });
28
+ };
29
+ if (Array.isArray(slide?.blocks)) {
30
+ slide.blocks.forEach((block, index) => {
31
+ if (isRecord(block)) walk(block, slidePath ? `${slidePath}.blocks.${index}` : `blocks.${index}`, 0, []);
32
+ });
33
+ }
34
+ for (const key of promotedRegionKeys) {
35
+ const region = slide?.[key];
36
+ if (isRecord(region)) walk(region, slidePath ? `${slidePath}.${key}` : key, 0, []);
37
+ }
38
+ }
39
+
40
+ export function slideIds(slide) {
41
+ const ids = typeof slide?.id === "string" ? [slide.id] : [];
42
+ if (isRecord(slide)) {
43
+ visitContentPayloads(slide, "", (payload) => {
44
+ if (typeof payload.id === "string") ids.push(payload.id);
45
+ });
46
+ }
47
+ return ids;
48
+ }
49
+
50
+ export function collectReservedPresentationIds(document) {
51
+ const ids = [];
52
+ for (const slide of document.slides ?? []) ids.push(...slideIds(slide));
53
+ return ids.filter(Boolean);
54
+ }
55
+
56
+ function remapNodeId(node, ids) {
57
+ if (typeof node.id !== "string") return;
58
+ const base = node.id;
59
+ let suffix = 2;
60
+ while (ids.has(node.id)) node.id = `${base}-${suffix++}`;
61
+ ids.add(node.id);
62
+ }
63
+
64
+ /** Remap slide and payload ids when inserting slides (shared document namespace). */
65
+ export function remapSlideTreeIds(slide, ids) {
66
+ remapNodeId(slide, ids);
67
+ visitContentPayloads(slide, "", (payload) => remapNodeId(payload, ids));
68
+ }
@@ -1,4 +1,5 @@
1
1
  import {richTextContent, formatRichTextRange, replaceRichTextRange} from './rich-text.js';
2
+ import { isAuthoringColorRef } from './color-authoring.js';
2
3
 
3
4
  /** Native SVG selection stays on the exact rendered glyphs; no HTML reflow. */
4
5
  export function createRichTextToolbar(root, {editor, report, beforeChange, onProperties, onTyping, validateChange, onChange}) {
@@ -80,7 +81,7 @@ export function createRichTextToolbar(root, {editor, report, beforeChange, onPro
80
81
  toolbar.append(input);return input;
81
82
  }
82
83
  const color=field('Text color');
83
- color.onchange=()=>{try {if(color.value&&!/^#(?:[\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i.test(color.value))throw new Error('Use a hex text color, such as #2563eb.');apply({color:color.value||null});}catch(error){report(error);}};
84
+ color.onchange=()=>{try {if(color.value&&!isAuthoringColorRef(color.value))throw new Error('Use a hex color, scheme slot or role, or var:<id> (for example #2563eb, accent2, text, or var:risk).');apply({color:color.value||null});}catch(error){report(error);}};
84
85
  const size=field('Font size (pt)','number');size.min='0.1';size.step='0.5';size.style.width='80px';
85
86
  size.onchange=()=>{try{apply({fontSize:size.value?Number(size.value):null});}catch(error){report(error);}};
86
87
  const family=field('Font family');family.onchange=()=>{try{apply({fontFamily:family.value||null});}catch(error){report(error);}};
@@ -17,8 +17,10 @@ export function createSchemaInspector(container, {editor,path='',onDraft,onCommi
17
17
  function check(){
18
18
  const validation=validateOpfDocument(draft);
19
19
  apply.disabled=!dirty||!validation.valid;
20
- status.textContent=dirty?'Draft changes · apply to save':'No pending changes';
21
- showError(validation.valid?'':validation.errors.slice(0,3).map(error=>`${error.path||'/'}: ${error.message}`).join('\n'));
20
+ const warningNote=validation.warnings?.length?` · ${validation.warnings.length} warning${validation.warnings.length===1?'':'s'}`:'';
21
+ status.textContent=dirty?`Draft changes · apply to save${warningNote}`:`No pending changes${warningNote}`;
22
+ const issues=[...(validation.valid?[]:validation.errors.slice(0,3).map(error=>`${error.path||'/'}: ${error.message}`)),...(validation.warnings?.slice(0,2).map(warning=>`${warning.path||'/'}: ${warning.message}`)??[])];
23
+ showError(issues.join('\n'));
22
24
  if(validation.valid)try{onDraft?.({document:structuredClone(draft),path:selected,dirty});}catch(error){showError(`Preview: ${error.message}`);onError?.(error);}
23
25
  }
24
26
  function mutate(operations,redraw=true){draft=applyJsonPatch(draft,operations);dirty=JSON.stringify(draft)!==base;if(redraw)render();check();}
@@ -0,0 +1,77 @@
1
+ import type { EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
2
+
3
+ /** The 14 pptx.gallery dimensions. */
4
+ export declare const SWITCH_DIMENSIONS: readonly [
5
+ "layouts",
6
+ "color-schemes",
7
+ "font-schemes",
8
+ "languages",
9
+ "backgrounds",
10
+ "narratives",
11
+ "charts",
12
+ "themes",
13
+ "audiences",
14
+ "tones",
15
+ "socials",
16
+ "headers-footers",
17
+ "blocks",
18
+ "image-treatments",
19
+ ];
20
+ export type SwitchDimension = (typeof SWITCH_DIMENSIONS)[number];
21
+
22
+ export interface DimensionSwitchOptions {
23
+ /** Required for layouts and charts; for design dimensions it scopes the switch to one slide (default: the deck). */
24
+ slideIndex?: number;
25
+ /** blocks: the complete block (`slides.0.blocks.1`) or one-payload slide/region to replace. charts: the block holding the chart (default: the slide's first chart). */
26
+ path?: string | string[];
27
+ /** A gallery item's catalog record, added inline in the same transaction when the id is not already defined. */
28
+ record?: Record<string, unknown> & { id: string };
29
+ /** Extra caller-loaded catalog records by kind. */
30
+ catalogs?: Record<string, unknown>;
31
+ catalogSources?: Record<string, unknown>;
32
+ /** themes: also write the theme's color scheme, font scheme, background and dimensions to the deck design (default true). */
33
+ bundle?: boolean;
34
+ /** Deck-scope design switches: remove slide-level values that would hide the switch. */
35
+ clearSlideOverrides?: boolean;
36
+ /** socials: which document field holds the handle (default "speaker") and, for an array, which entry (default 0). */
37
+ owner?: "speaker" | "organization";
38
+ index?: number;
39
+ /** blocks: media source for the image and video kinds. */
40
+ source?: string;
41
+ /** Session change metadata (switchDimension only). */
42
+ meta?: Record<string, unknown>;
43
+ }
44
+
45
+ export type DimensionSwitchValue =
46
+ | string
47
+ | string[]
48
+ | { platform: string; handle: string }
49
+ | { header?: unknown; footer?: unknown }
50
+ | { slideImage?: unknown; imageFill?: string | null }
51
+ | Record<string, unknown>;
52
+
53
+ export interface PreparedDimensionSwitch {
54
+ dimension: SwitchDimension;
55
+ scope: "deck" | "slide" | "block";
56
+ slideIndex?: number;
57
+ document: unknown;
58
+ patches: JsonPatchOperation[];
59
+ changed: boolean;
60
+ /** Slides whose own design hides a deck-level switch. */
61
+ shadowed: number[];
62
+ }
63
+
64
+ export interface DimensionSwitchChange extends Omit<EditorChange, "document" | "patches"> {
65
+ document: unknown;
66
+ patches: JsonPatchOperation[];
67
+ dimension: SwitchDimension;
68
+ scope: "deck" | "slide" | "block";
69
+ slideIndex?: number;
70
+ changed: boolean;
71
+ shadowed: number[];
72
+ }
73
+
74
+ /** Compute and validate the patch for one dimension without changing any session. */
75
+ export declare function prepareDimensionSwitch(document: unknown, dimension: SwitchDimension, value: DimensionSwitchValue, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
76
+ /** Apply one dimension switch to a session as a single undoable transaction. */
77
+ export declare function switchDimension(editor: EditorSession, dimension: SwitchDimension, value: DimensionSwitchValue, options?: DimensionSwitchOptions): DimensionSwitchChange;