@openpresentation/opf-editor 0.17.0 → 0.18.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 CHANGED
@@ -10,13 +10,13 @@ Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs
10
10
  | --- | --- |
11
11
  | The canvas, the preview, SVG downloads in a browser (the host serves its own font files) | nothing |
12
12
  | `loadFonts` from `/fonts-node` (a server, a test, a build script such as `npm run build:playground`) | the font packages of the pack: `@expo-google-fonts/roboto` and `roboto-mono` for `base`; also `arimo`, `caladea`, `cousine`, `gelasio`, `tinos` and `noto-sans` for `office`; a script face (`scripts: 'auto'`) needs its `@expo-google-fonts/noto-*` package. A missing one fails with `font-resource-unavailable` and names it |
13
- | `svgToPng` / `svgToPdf` from the Node entries (`/png`, `/pdf`) | `@resvg/resvg-js` (and `sharp`, `pdf-lib` as the renderer's README says); a missing one fails with `converter-missing` |
14
- | `exportDeck` PDF (vector), PNG, SVG | nothing: the browser entry `@openpresentation/opf-render/export-browser` imports no converter |
15
- | `exportDeck` raster PDF (`pdfMode: "raster"`) | `pdf-lib`, imported by the page and passed as the `pdfLib` option (`import * as pdfLib from "pdf-lib"`); without it the export rejects with `converter-missing` |
13
+ | `toPng` / `toPdf` from the Node entries (`/png`, `/pdf`) | `@resvg/resvg-js` (and `sharp`, `pdf-lib` as the renderer's README says); a missing one fails with `converter-missing` |
14
+ | `convert` PDF (vector), PNG, SVG | nothing: the browser entry `@openpresentation/opf-render/export-browser` imports no converter |
15
+ | `convert` raster PDF (`raster: true`) | `pdf-lib`, imported by the page and passed as the `pdfLib` option (`import * as pdfLib from "pdf-lib"`); without it the conversion rejects with `converter-missing` |
16
16
 
17
17
  ```js
18
18
  const pdfLib = await import("pdf-lib");
19
- await exportDeck(editor.presentation, { format: "pdf", pdfMode: "raster", pdfLib, fonts });
19
+ await convert(editor.presentation, { format: "pdf", raster: true, pdfLib, fonts });
20
20
  ```
21
21
 
22
22
  The playground imports `pdf-lib` for you when its raster PDF choice is used.
@@ -37,7 +37,7 @@ The release after 0.14.2 moves the editor onto core 0.15 (a breaking, clean spec
37
37
  ```
38
38
 
39
39
  `mergeCatalogs(...lists)` is the one merge (the session's list first, the first catalog per `source` wins); `catalogsFor(editor, options)` is the list for one call. Pickers list core's `catalogRecords` and write the reference it gives, `id` or `name:id`.
40
- - **Save and export embed.** `prepareSave`/`saveDocument`, `serializeOpfTransfer` (presentation and slide scope) and `exportDeck` call core's `embed`; the playground's OPF download and PPTX export do too.
40
+ - **Save and export embed.** `prepareSave`/`saveDocument`, `serializeOpfTransfer` (presentation and slide scope) and `convert` (`/export`) call core's `embed`; the playground's OPF download and PPTX export do too.
41
41
  - **Paste** (`prepareOpfImport`, insert mode) uses core's `copySlides`: groups match by `source`, identical records are reused, and the result's `renamed` (`custom-conflict`, `catalog-revision`, `group-name`) and `addedGroups` tell the user what changed.
42
42
  - **Update from catalog** is an explicit action: `checkCatalogUpdates(presentation, { catalogs })` lists the embedded records whose catalog record differs, and `applyCatalogUpdate(editor, { refs })` applies only the approved ones as one undoable step. The design panel's `catalog` section runs both and lists the `opf/unresolved-reference` and `opf/undeclared-catalog` findings (`referenceFindings(editor.validation)`).
43
43
  - **Editing a catalog's record forks it.** An edit of a layout, theme, colour scheme or font scheme embedded under `catalogs.default` or a named group (from a panel, Properties, Source or any `applyPatch`) moves it into `catalogs.custom` as `<id>-custom` (or the id in `meta.forkIds["<group>:<kind>:<id>"]`), rewrites every reference to it and drops the original, in the same undo step. The change and the session event carry `forked` and a `notice` ("… is now this presentation's own; it no longer receives catalog updates"); approved catalog updates and `moveToCustom` change records in place (`meta.catalogRecordEdit: "in-place"`). `moveToCustom(editor, { group, kind, id }, { id })` is the explicit form, offered for `opf/catalog-record-not-in-source`.
@@ -122,7 +122,7 @@ await canvas.ready;
122
122
  // canvas.destroy() when unmounting.
123
123
  ```
124
124
 
125
- Load the same font bytes into the browser with `loadFonts` from `@openpresentation/opf-render/fonts-browser` before mounting; the handle it returns is the one `fonts` option of the canvas, `exportDeck`, `editor.composeSlide` and `editor.paginateSlide`. The host owns font URLs, storage and collaboration. For non-Latin documents, pass the pinned script pack's location as `scriptBaseUrl` and the canvas calls `fonts.ensure(presentation)` after edits (`scripts: 'auto'`, FF-19): only the faces for the scripts a document draws are fetched, once, hash-verified, for example Noto Sans JP for Japanese; a Latin-only document fetches none. The playground does this from `./script-fonts/`, which `npm run build:playground` fills with the pinned faces. `onDraft` provides live document drafts; the session only changes on commit. Escape cancels. Concurrent edits to the selected payload cancel a stale draft.
125
+ Load the same font bytes into the browser with `loadFonts` from `@openpresentation/opf-render/fonts-browser` before mounting; the handle it returns is the one `fonts` option of the canvas, `convert` (`/export`), `editor.composeSlide` and `editor.paginateSlide`. The host owns font URLs, storage and collaboration. For non-Latin documents, pass the pinned script pack's location as `scriptBaseUrl` and the canvas calls `fonts.ensure(presentation)` after edits (`scripts: 'auto'`, FF-19): only the faces for the scripts a document draws are fetched, once, hash-verified, for example Noto Sans JP for Japanese; a Latin-only document fetches none. The playground does this from `./script-fonts/`, which `npm run build:playground` fills with the pinned faces. `onDraft` provides live document drafts; the session only changes on commit. Escape cancels. Concurrent edits to the selected payload cancel a stale draft.
126
126
 
127
127
  Fonts load before pixels (FF-41). A document can need faces the browser has not loaded yet: script faces for the languages it draws and vendored preview faces for the font families it resolves (Intos for Aptos, Open Sans, Barlow). The handle knows what is missing (`fonts.pending(presentation)`) and loads it (`await fonts.ensure(presentation)`), so the canvas needs no separate gate: with the handle as `fonts` it never renders such a document early: on every path (editor changes, undo and redo, imports, dimension switches, slide changes, in-progress edits) it shows "Loading fonts…", loads the faces and then renders. A load that fails is reported through `onFonts`/`onError` and offers a retry button; nothing retries by itself, and the failed document is not drawn.
128
128
 
@@ -137,7 +137,7 @@ const canvas = createCanvasEditor(container, { editor, fonts });
137
137
  // Other renders of your own: whenFontsReady(fonts, presentation, { ready: draw, failed: showError, loading: showSpinner })
138
138
  ```
139
139
 
140
- `session.composeSlide(index, { fonts })` and `session.paginateSlide(index, { fonts })` take the same handle and resolve the slide's canvas, layout, theme, colour scheme and font families with core's `resolveSlideContext` (slide design, then deck design, then theme, then the engine default: the order the renderer and the PowerPoint export use) and the session's registered catalogs, so the editor composes what is drawn and exported. A reference that resolves nowhere never throws: the slide composes automatically or with core's `ENGINE_DEFAULT_*`, and `onDiagnostic` hears each `unresolved-reference` diagnostic. The handle's `textMeasurement` is strict: for a document with a script the design font lacks (Japanese under Aptos) pass `{ fonts: { textMeasurement: createScriptTextMeasurement(fonts.textMeasurement, resolveScriptFonts(presentation, { slideIndex })) } }` (`createScriptTextMeasurement` is from `@openpresentation/opf-render/fonts`, `resolveScriptFonts` from `@openpresentation/opf/composition`), as `renderSvg` does internally.
140
+ `session.composeSlide(index, { fonts })` and `session.paginateSlide(index, { fonts })` take the same handle and resolve the slide's canvas, layout, theme, colour scheme and font families with core's `resolveSlideContext` (slide design, then deck design, then theme, then the engine default: the order the renderer and the PowerPoint export use) and the session's registered catalogs, so the editor composes what is drawn and exported. The slide it composes is `resolveSlideContext(...).slide`, so the organization logo references in its image fields (`var:organization.logo.icon` in a header or footer zone, an image block or a slide image) are already the right artwork for the slide's background (`onLight` on a light slide, `onDark` on a dark one); the cover and section logo and the zone parts come back in `composeSlide(index).logo` and `.furniture.parts` (each part is drawn from its `box`). A reference that resolves nowhere never throws: the slide composes automatically or with core's `ENGINE_DEFAULT_*`, and `onDiagnostic` hears each `unresolved-reference` diagnostic. The handle's `textMeasurement` is strict: for a document with a script the design font lacks (Japanese under Aptos) pass `{ fonts: { textMeasurement: createScriptTextMeasurement(fonts.textMeasurement, resolveScriptFonts(presentation, { slideIndex })) } }` (`createScriptTextMeasurement` is from `@openpresentation/opf-render/fonts`, `resolveScriptFonts` from `@openpresentation/opf/composition`), as `toSvg` does internally.
141
141
 
142
142
  These APIs were introduced in 0.1.0. Version 0.7.0 requires core 0.10.0 and renderer 0.8.0 for the canvas, including shared accepted geometry, styled/merged cells, rich table values, headers and content-aware row heights. See the OPF repository’s `docs/live-editor.md` for setup, the support matrix and roadmap. `pnpm pack:ecosystem` in that repository also prepares local preview tarballs for coordinated development.
143
143
 
@@ -189,10 +189,10 @@ import {
189
189
  createEditorSession,
190
190
  createSvgTraceBinding
191
191
  } from "@openpresentation/opf-editor";
192
- import { renderSlideSvg } from "@openpresentation/opf-render";
192
+ import { toSvg } from "@openpresentation/opf-render";
193
193
 
194
194
  const editor = createEditorSession(opfDocument, { rejectInvalid: true });
195
- const svg = renderSlideSvg(editor.presentation, 0, { trace: true, fonts });
195
+ const svg = toSvg(editor.presentation, 1, { trace: true, fonts });
196
196
 
197
197
  preview.innerHTML = svg;
198
198
  const binding = createSvgTraceBinding(preview.querySelector("svg"), editor, {
@@ -336,13 +336,16 @@ The playground's Content tab mounts the block-level and slide-structure actions
336
336
  `@openpresentation/opf-editor/design-options` edits the design-level settings that used to need All properties. Each call is one validated patch and one undo step, at the deck or on one slide with `slideIndex`; `null` removes a value so it is inherited again. `prepare...` forms return the patch without touching a session.
337
337
 
338
338
  ```js
339
- import { setDesignOption, setLogoVariant, setHeaderFooterZone, insertZoneValue, getDesignOption, designWarnings } from "@openpresentation/opf-editor/design-options";
339
+ import { setDesignOption, setOrganizationLogo, setHeaderFooterZone, insertZoneValue, insertZoneLogo, getDesignOption, designWarnings } from "@openpresentation/opf-editor/design-options";
340
340
 
341
341
  setDesignOption(editor, "titleAlignment", "center", { slideIndex: 2 }); // /slides/2/design/titleAlignment
342
342
  setDesignOption(editor, "watermark", { src: "asset:mark", opacity: 0.1 }); // merges into an existing watermark; false hides an inherited one
343
- setLogoVariant(editor, "light", "asset:logo-white"); // default stays a bare source; more variants make a LogoSet
344
- setHeaderFooterZone(editor, "footer", "right", { text: "{{slide.number}} / {{deck.slideCount}}", logo: true });
343
+ setOrganizationLogo(editor, "all", "asset:logo"); // organization.logo: one image for every shape
344
+ setOrganizationLogo(editor, "icon", "asset:mark-white", { background: "onDark" }); // a shape's own image for dark backgrounds (the rest keeps what it had)
345
+ setDesignOption(editor, "logo", { organization: "beta", shape: "icon" }); // design.logo = "var:organization.beta.logo.icon"; false draws none, null the primary organization's
346
+ setHeaderFooterZone(editor, "footer", "right", { text: "{{slide.number}} / {{deck.slideCount}}" });
345
347
  insertZoneValue(editor, "footer", "left", "organization.name", { start: 0, end: 0 }); // puts {{organization.name}} at the start of the left text
348
+ insertZoneLogo(editor, "header", "left"); // the zone's image becomes var:organization.logo.icon
346
349
  ```
347
350
 
348
351
  | Option | Writes | Values |
@@ -354,12 +357,12 @@ insertZoneValue(editor, "footer", "left", "organization.name", { start: 0, end:
354
357
  | `contentBox` | `design.contentBox` | `true`, `false` |
355
358
  | `imageFit` | `design.imageFit` | `cover`, `contain`, `stretch`: the fit of image blocks that choose none |
356
359
  | `accentFont` | `design.fontScheme.accent` | a family name; the font scheme becomes its object form and collapses back to the bare id when the accent is cleared |
357
- | `logo` | `design.logo` | a source, an Asset object or a LogoSet; `setLogoVariant` edits one of the 12 variants |
358
- | `organizationLogo` | `organization.logo` (deck only; `index` picks an organization) | a source or Asset object |
360
+ | `logo` | `design.logo` | which organization's logo covers, sections and picture bullets draw: `null` (the primary organization's, the default), `false` (none), a reference such as `"var:organization.beta.logo.icon"`, or `{ organization, shape }` (an organization id and a shape) to write one. A reference to an organization the deck does not have is refused (`unknown-organization`). `readLogoChoice` reads it as `{ mode: "primary" \| "none" \| "reference" \| "custom", organization?, shape?, scope, inherited }`; `parseLogoReference` and `logoReference` convert between the string and `{ organization, shape }` |
361
+ | the organization's logo | `organization.logo` (deck only; not a `setDesignOption` option) | `setOrganizationLogo(editor, shape, source, { background, organization })` sets or clears (`null`) it: `shape` is `"all"` (one image for every shape: `organization.logo` is that image) or one of `LOGO_SHAPES` (`full`, `stacked`, `icon`, `wordmark`); `background` is `"both"` (default), `"onLight"` or `"onDark"` (a different image on light or dark slides; the other keeps what the shape had, and a plain image counts as both); `organization` is an index or id (default: the primary organization, `role: "primary"` else the first). A bare logo is the full logo, so setting a shape keeps it as `full`, and a lone plain `full` collapses back to the bare image. `readOrganizationLogo` reads `{ organization, all, shapes }` and `listOrganizations` lists `{ index, id, name, primary, hasLogo }` |
359
362
  | `watermark` | `design.watermark` | `false`, a source, `{ src, opacity }` or the text stamp `{ text, opacity }` (fields merge; a lone source stays a bare source; setting `text` drops `src` and setting `src` drops `text`: a watermark is one or the other) |
360
- | header and footer zones | `design.header` / `design.footer` `.left/.center/.right` | `setHeaderFooterZone` merges every part a zone can hold (`text`, `logo`, `image`, `date` (true, or a fixed date) and `dateFormat`, `socials`; `ZONE_FIELDS`) into one zone, checking each value. Generated values are `{{ }}` variables written in `text`, in the order the author wants and with `\n` between lines: `{{slide.number}}`, `{{deck.slideCount}}`, `{{slide.section}}`, `{{organization.name}}`, `{{speaker.name}}`, `{{deck.name}}` (the entries of `ZONE_VALUES`, the panel's "Insert value" menu). `insertZoneValue(editor, which, zone, name, { start, end, text, slideIndex })` puts one token into a zone's text at UTF-16 offsets (a selection is replaced; the end by default; `text` when a text box holds typing that is not committed yet) as one undoable, validated edit, and `prepareZoneValue` returns the patch without a session. The 0.16 fields `organization`, `speaker`, `section`, `slideNumber` and `slideNumberFormat` are gone with no alias (`{current}` and `{total}` too), and are refused with a message that names the variable to write instead. A removed field, an emptied zone and an emptied header are all deleted rather than left as `{}`; a removed field, an emptied zone and an emptied header are all deleted rather than left as `{}`. A slide's own header or footer replaces the deck's whole one, so the first edit on a slide starts from a copy of the deck's (its other zones stay), and a slide emptied that way hides the furniture (`false`) instead of inheriting it again |
363
+ | header and footer zones | `design.header` / `design.footer` `.left/.center/.right` | `setHeaderFooterZone` merges every part a zone can hold (`text`, `image`, `date` (true, or a fixed date) and `dateFormat`, `socials`; `ZONE_FIELDS`) into one zone, checking each value. Generated values are `{{ }}` variables written in `text`, in the order the author wants and with `\n` between lines: `{{slide.number}}`, `{{deck.slideCount}}`, `{{slide.section}}`, `{{organization.name}}`, `{{speaker.name}}`, `{{deck.name}}` (the entries of `ZONE_VALUES`, the panel's "Insert value" menu). `insertZoneValue(editor, which, zone, name, { start, end, text, slideIndex })` puts one token into a zone's text at UTF-16 offsets (a selection is replaced; the end by default; `text` when a text box holds typing that is not committed yet) as one undoable, validated edit, and `prepareZoneValue` returns the patch without a session. The logo is the zone's `image`, a reference to the organization's logo: `insertZoneLogo(editor, which, zone, { shape, organization, slideIndex })` writes `var:organization.logo.<shape>` (`icon` by default, which fits a zone; `full` is plain `var:organization.logo`; `organization: "beta"` writes `var:organization.beta.logo.<shape>`) as one undoable, validated edit, replacing an image the zone had, and `prepareZoneLogo` returns the patch without a session. Which artwork is drawn (`onLight` or `onDark`) follows each slide's background. The 0.17 `logo: true` and the 0.16 fields `organization`, `speaker`, `section`, `slideNumber` and `slideNumberFormat` are gone with no alias (`{current}` and `{total}` too), and are refused with a message that says what to write instead. A removed field, an emptied zone and an emptied header are all deleted rather than left as `{}`; a removed field, an emptied zone and an emptied header are all deleted rather than left as `{}`. A slide's own header or footer replaces the deck's whole one, so the first edit on a slide starts from a copy of the deck's (its other zones stay), and a slide emptied that way hides the furniture (`false`) instead of inheriting it again |
361
364
 
362
- A deck-scope change reports `shadowed` slides whose own design hides it (`clearSlideOverrides: true` removes those values in the same transaction). Results carry `warnings`: a header or footer zone with `logo: true`, or picture bullets, with no logo to draw (no slide, deck or primary-organization logo) is reported as `unresolved-logo`, and a zone that shows the organization's social profiles when there are none, or whose text uses a deck-wide variable the document has no value for (`{{organization.name}}` without an organization, `{{speaker.name}}` without a named speaker), or `{{slide.section}}` on a slide without a section, as `unresolved-content`, before export, as `designWarnings(presentation, slideIndex)` does for the current document. `DESIGN_OPTIONS` describes every option for a generic panel, and `getDesignOption` reads `{ value, scope, inherited }`.
365
+ A deck-scope change reports `shadowed` slides whose own design hides it (`clearSlideOverrides: true` removes those values in the same transaction). Results carry `warnings`: a zone image or `design.logo` that names a logo no organization has, or picture bullets (they use the icon shape) with no logo to draw, is reported as `unresolved-logo`, and a zone that shows the organization's social profiles when there are none, or whose text uses a deck-wide variable the document has no value for (`{{organization.name}}` without an organization, `{{speaker.name}}` without a named speaker), or `{{slide.section}}` on a slide without a section, as `unresolved-content`, before export, as `designWarnings(presentation, slideIndex)` does for the current document. `DESIGN_OPTIONS` describes every option for a generic panel, and `getDesignOption` reads `{ value, scope, inherited }`.
363
366
 
364
367
  ### Image uploads (RR-06)
365
368
 
@@ -367,13 +370,13 @@ A deck-scope change reports `shadowed` slides whose own design hides it (`clearS
367
370
 
368
371
  ```js
369
372
  import { applyImageUpload } from "@openpresentation/opf-editor/assets";
370
- import { prepareLogoVariant } from "@openpresentation/opf-editor/design-options";
373
+ import { prepareOrganizationLogo } from "@openpresentation/opf-editor/design-options";
371
374
 
372
- const change = await applyImageUpload(editor, file, (reference, presentation) => prepareLogoVariant(presentation, "light", reference), { alt: "Acme logo" });
373
- change.assetId; // "acme-logo": the document now has assets["acme-logo"] = { src: "data:image/png;base64,...", mediaType, title, alt } and design.logo.light = "asset:acme-logo"
375
+ const change = await applyImageUpload(editor, file, (reference, presentation) => prepareOrganizationLogo(presentation, "icon", reference, { background: "onDark" }), { alt: "Acme logo" });
376
+ change.assetId; // "acme-logo": the document now has assets["acme-logo"] = { src: "data:image/png;base64,...", mediaType, title, alt } and organization.logo.icon.onDark = "asset:acme-logo"
374
377
  ```
375
378
 
376
- `build(reference, presentation)` is any `prepare...` function of this package, so the same upload works for the logo (all 12 variants), organization logo, watermark, a background (`prepareBackground`) and a header/footer zone image. The file is checked before anything changes: PNG, JPEG, GIF, WebP or SVG by its bytes (a `.jpg` that is really a PNG, a text file, an empty file and an SVG with script are refused), and at most `maxBytes` (default 2 MiB, `DEFAULT_MAX_IMAGE_BYTES`) with a message that says what to do. A host that stores images elsewhere passes `onAddAsset({ name, mediaType, bytes, size, alt, file })` and returns the reference to use (a web address, or an `asset:` id it added); nothing is then added to `assets`. `setAssetAlt` edits an asset's alt text as one step.
379
+ `build(reference, presentation)` is any `prepare...` function of this package, so the same upload works for the organization logo (one image for every shape, or each shape for both, light or dark backgrounds), watermark, a background (`prepareBackground`) and a header/footer zone image. The file is checked before anything changes: PNG, JPEG, GIF, WebP or SVG by its bytes (a `.jpg` that is really a PNG, a text file, an empty file and an SVG with script are refused), and at most `maxBytes` (default 2 MiB, `DEFAULT_MAX_IMAGE_BYTES`) with a message that says what to do. A host that stores images elsewhere passes `onAddAsset({ name, mediaType, bytes, size, alt, file })` and returns the reference to use (a web address, or an `asset:` id it added); nothing is then added to `assets`. `setAssetAlt` edits an asset's alt text as one step.
377
380
 
378
381
  ### Backgrounds (RR-06)
379
382
 
@@ -444,7 +447,7 @@ Sections are OPF's `section` label on each slide: consecutive slides with the sa
444
447
  import { createSlideManager } from "@openpresentation/opf-editor/slide-manager";
445
448
  const manager = createSlideManager(document.querySelector("#slide-list"), {
446
449
  editor, getSlideIndex: () => current, setSlideIndex: (index) => { current = index; redraw(); },
447
- renderThumbnail: (deck, index) => renderSlideSvg(deck, index, { fonts, trace: false }), toolbar: document.querySelector("#slide-toolbar"),
450
+ renderThumbnail: (deck, index) => toSvg(deck, index + 1, { fonts, trace: false }), toolbar: document.querySelector("#slide-toolbar"),
448
451
  contentActions, // optional: { splitSlideByBlocks, mergeSlides } from "@openpresentation/opf-editor/content-actions"
449
452
  });
450
453
  editor.subscribe(() => manager.render());
@@ -471,12 +474,12 @@ Chart columns and table headers may be `DataColumn` objects (`{ name, format }`;
471
474
 
472
475
  ```js
473
476
  import { setGridColumnFormat, setChartMapping, detachGridDataset } from "@openpresentation/opf-editor/data-grid";
474
- import { prepareDatasetImport, importData } from "@openpresentation/opf-editor/data";
477
+ import { prepareDatasetImport, ingest } from "@openpresentation/opf-editor/data";
475
478
 
476
479
  setGridColumnFormat(editor, "slides.3.blocks.0.chart", 1, "$#,##0.0"); // header "Revenue" becomes { name: "Revenue", format }; null clears it
477
480
  setChartMapping(editor, "slides.3.blocks.0.chart", { category: "Region", series: ["Revenue"] });
478
481
  detachGridDataset(editor, "slides.3.blocks.1.table"); // its own copy instead of the shared dataset
479
- const stored = prepareDatasetImport(editor.presentation, importData(csv, { as: "chart" }), { id: "revenue" });
482
+ const stored = prepareDatasetImport(editor.presentation, ingest(csv, { as: "chart" }), { id: "revenue" });
480
483
  editor.applyPatch([...stored.patches, { op: "add", path: "/slides/-", value: { id: "rev", title: "Revenue", ...stored.content } }]);
481
484
  ```
482
485
 
@@ -500,7 +503,7 @@ const selectionControls = createDesignControls(contentPanel, { editor, getSlideI
500
503
  controls.refresh(); // when the slide or the selection changes
501
504
  ```
502
505
 
503
- Sections: `look` (theme, color scheme, font scheme, language, this slide's layout), `background` (type, theme slot, solid color, gradient with its stops, image with fit, focus, alt text, recolor and overlay, any of the 54 patterns, opacity; a draft until Apply, with Remove), `header-footer` (choose header or footer and a zone, then every part the zone supports: a text box with an "Insert value" menu that puts the slide number, slide count, section, organization, speaker or deck name at the cursor as a `{{ }}` variable (Enter commits, Shift+Enter starts a new line), logo, image, social profiles, and the current or fixed date and format, with a summary of the zones in use), `brand` (logo variants, organization logo, picture bullets, accent font, watermark), `layout-options` (alignment, direction, primary chart, content box, default image fit), `info` (narrative, tone, audience, socials), `selection` (content type with a loss report, list levels, group of metrics, group and ungroup, image to background or watermark, replacement, chart type), `image` (the selected image block's fit, focus, shape, corner radius, border, opacity, recolor, overlay, aspect ratio and placement), `catalog` (reference findings and the explicit "Update from catalog"), `slide-content` (blocks to regions and back, design images back into the content) and `table` (style, merge, split, cell fill and alignment). The panel is native `details`, `fieldset`, `select`, checkbox and text controls with a label on each, so it works with the keyboard and with screen readers: groups open with Enter or Space, a select changes with the arrow keys, text fields commit on Enter or when you leave them, and results and refusals are announced in `role="status"` and `role="alert"` regions. A refused change (an invalid value, text a merge would hide) is explained, changes nothing, and puts the field back. "Applies to" switches the design controls between the whole presentation and the current slide, and a control shows when a slide value is "set on this slide" or "from the presentation". The panel offers only conversions that exist, shows what a conversion loses before you choose it, and lists why an unavailable one is unavailable.
506
+ Sections: `look` (theme, color scheme, font scheme, language, this slide's layout), `background` (type, theme slot, solid color, gradient with its stops, image with fit, focus, alt text, recolor and overlay, any of the 54 patterns, opacity; a draft until Apply, with Remove), `header-footer` (choose header or footer and a zone, then every part the zone supports: a text box with an "Insert value" menu that puts the slide number, slide count, section, organization, speaker or deck name at the cursor as a `{{ }}` variable (Enter commits, Shift+Enter starts a new line), an image with an "Insert logo" action (a shape and, for several organizations, an organization; it writes the logo reference as the zone's image), social profiles, and the current or fixed date and format, with a summary of the zones in use), `brand` (the organization logo: one image for every shape, or each of the four shapes for both, light and dark backgrounds, for the organization you choose; which organization's logo and shape covers, sections and picture bullets draw, or none; picture bullets, accent font, watermark), `layout-options` (alignment, direction, primary chart, content box, default image fit), `info` (narrative, tone, audience, socials), `selection` (content type with a loss report, list levels, group of metrics, group and ungroup, image to background or watermark, replacement, chart type), `image` (the selected image block's fit, focus, shape, corner radius, border, opacity, recolor, overlay, aspect ratio and placement), `catalog` (reference findings and the explicit "Update from catalog"), `slide-content` (blocks to regions and back, design images back into the content) and `table` (style, merge, split, cell fill and alignment). The panel is native `details`, `fieldset`, `select`, checkbox and text controls with a label on each, so it works with the keyboard and with screen readers: groups open with Enter or Space, a select changes with the arrow keys, text fields commit on Enter or when you leave them, and results and refusals are announced in `role="status"` and `role="alert"` regions. A refused change (an invalid value, text a merge would hide) is explained, changes nothing, and puts the field back. "Applies to" switches the design controls between the whole presentation and the current slide, and a control shows when a slide value is "set on this slide" or "from the presentation". The panel offers only conversions that exist, shows what a conversion loses before you choose it, and lists why an unavailable one is unavailable.
504
507
 
505
508
  In React (or Svelte, or anything else) mount it into a ref and destroy it on unmount; it needs no framework runtime:
506
509
 
@@ -512,7 +515,7 @@ useEffect(() => {
512
515
  // call controls.refresh() (keep it in a ref) when slide or selected changes
513
516
  ```
514
517
 
515
- The playground mounts both panels (the Design tab, and the selection area of the Content tab). One click on text still enters text editing; the panels follow the selection after Escape. Every image field (logo variants, organization logo, watermark, background image and header/footer zone image) takes an asset reference (suggested from the document's assets), a web address or a data address, and has a file picker and an alt-text field: the chosen file is validated (PNG, JPEG, GIF, WebP or SVG, up to `maxImageBytes`, 2 MiB by default), added to `assets` and used in one undo step, or handed to your app through `onAddAsset`. Alt text is saved on the asset; typed before an upload it is used for that upload.
518
+ The playground mounts both panels (the Design tab, and the selection area of the Content tab). One click on text still enters text editing; the panels follow the selection after Escape. Every image field (organization logo, watermark, background image and header/footer zone image) takes an asset reference (suggested from the document's assets), a web address or a data address, and has a file picker and an alt-text field: the chosen file is validated (PNG, JPEG, GIF, WebP or SVG, up to `maxImageBytes`, 2 MiB by default), added to `assets` and used in one undo step, or handed to your app through `onAddAsset`. Alt text is saved on the asset; typed before an upload it is used for that upload.
516
519
 
517
520
  Run `npm run test:design-controls-browser` (after `npm run build:playground`) for the real-browser check of every control, its keyboard operation and its undo.
518
521
 
@@ -551,18 +554,18 @@ A template is an OPF file with variables (`{{id}}` tokens and `var:id` reference
551
554
 
552
555
  ```js
553
556
  import { createTemplatePanel } from '@openpresentation/opf-editor/template-panel';
554
- import { renderSlideSvg } from '@openpresentation/opf-render/svg';
557
+ import { toSvg } from '@openpresentation/opf-render/svg';
555
558
 
556
559
  const panel = createTemplatePanel(container, {
557
560
  editor,
558
561
  // The live preview: the template drawn with the values typed so far (unfilled variables show their example).
559
- renderPreview: ({ presentation, variables, slideIndex }) => renderSlideSvg(presentation, slideIndex, { fonts, variables }),
562
+ renderPreview: ({ presentation, variables, slideIndex }) => toSvg(presentation, slideIndex + 1, { fonts, variables }),
560
563
  getTarget: () => ({ path: selectedPath, start, end }), // the text field a token is inserted into; omit to hide that section
561
564
  onApply: () => redraw(),
562
565
  });
563
566
  ```
564
567
 
565
- The panel lists every variable with the input its kind needs (text, number, date, color, link, one-entry-per-line list, and an image source with an asset pick or an uploaded file, 5 MB at most), marks which are filled, defaulted, optional or still needed, says where each is used, rejects a bad value in place, and previews the result as values change. **Fill the presentation** resolves the variables and replaces the document with the concrete deck as one validated, undoable edit (one Undo restores the template); **Fill what is ready** keeps the unfilled variables declared. **Insert a variable into text** inserts `{{id}}` into the selected text, optionally declaring a new variable in the same edit. A **Built-in variables** section lists the values the document supplies itself (`deck.*`, `speaker.*`, `speakers`, `organization.*`, and `speaker.<id>.*` / `organization.<id>.*` by id), read-only, each with its current value or "not set" and where it is used; they are filled from the document's own fields, not in the panel, and the same names appear in the insert list so `{{speaker.name}}` can be inserted without declaring anything. The three slide-scoped built-ins (`slide.number`, `slide.section`, `deck.slideCount`; `scope: "slide"` in `listBuiltins`) come last and have no single value: the panel shows them as "varies per slide" (and notes when no slide has a section), because each slide gets its own when it is drawn or exported. A checkbox marks the document as a template.
568
+ The panel lists every variable with the input its kind needs (text, number, date, color, link, one-entry-per-line list, and an image source with an asset pick or an uploaded file, 5 MB at most), marks which are filled, defaulted, optional or still needed, says where each is used, rejects a bad value in place, and previews the result as values change. **Fill the presentation** resolves the variables and replaces the document with the concrete deck as one validated, undoable edit (one Undo restores the template); **Fill what is ready** keeps the unfilled variables declared. **Insert a variable into text** inserts `{{id}}` into the selected text, optionally declaring a new variable in the same edit. A **Built-in variables** section lists the values the document supplies itself (`deck.*`, `speaker.*`, `speakers`, `organization.*`, and `speaker.<id>.*` / `organization.<id>.*` by id), read-only, each with its current value or "not set" and where it is used; they are filled from the document's own fields, not in the panel, and the same names appear in the insert list so `{{speaker.name}}` can be inserted without declaring anything. The three slide-scoped text built-ins (`slide.number`, `slide.section`, `deck.slideCount`; `scope: "slide"` in `listBuiltins`) come last and have no single value: the panel shows them as "varies per slide" (and notes when no slide has a section), because each slide gets its own when it is drawn or exported. After them come the organization logos (`organization.logo`, `.stacked`, `.icon`, `.wordmark`, and the same under `organization.<id>.logo` for each organization; `kind: "image"`, `scope: "slide"`), read-only: they are images placed as a whole image field (`var:organization.logo.icon`), not `{{ }}` text, so the panel shows the `var:` reference, "light or dark artwork per slide" and "no logo yet" when the organization has none, and leaves them out of the insert list. A checkbox marks the document as a template.
566
569
 
567
570
  The headless pieces are usable on their own: `listTemplateFields(presentation, values)`, `templateStatus`, `previewTemplate`, `createTemplateFill(editor)` (`set`, `setText`, `clear`, `reset`, `preview`, `apply({partial})`), `declareVariable`, `setTemplate`, `insertVariableToken(editor, path, id, {start, end, runIndex, format, declare})`, `variableToken` (also for built-in names), `listBuiltins(document)` (the built-ins with kind, label, scope, value, availability and uses), `suggestVariableId`. Every write goes through the session. The canvas draws a template as authored (`renderOptions.variables` defaults to `false`), so its tokens stay visible and an inline edit never overwrites one with resolved text; the panel's preview draws the resolved deck. The slide-scoped built-ins are the exception on both: the renderer substitutes them for the slide it draws (its own number, section and the deck's count), and `editor.composeSlide(index, { slideNumber, slideCount })` composes that substituted slide, while the document and an inline edit keep the token. The playground adds a **Fill template** button.
568
571
 
@@ -610,28 +613,33 @@ Findings show a severity word, the slide, the rule id and, for a hosted reviewer
610
613
 
611
614
  ## PDF, PNG and SVG downloads (RR-23)
612
615
 
613
- `@openpresentation/opf-editor/export` turns the deck into a download in the page, next to the PowerPoint export; the playground's "PDF · PNG · SVG" button is a thin dialog over it.
616
+ `convert(deck, { format })` from `@openpresentation/opf-editor/export` turns the deck into files in the page, next to the PowerPoint export; the playground's "PDF · PNG · SVG" button is a thin dialog over it. It has the name and the result of core's in-memory `convert` (OPF 0.18, RR-73): `{ files, findings }`, core's `ConvertResult`.
614
617
 
615
618
  ```js
616
- import { exportDeck } from "@openpresentation/opf-editor/export";
617
- const result = await exportDeck(editor.presentation, {
619
+ import { convert } from "@openpresentation/opf-editor/export";
620
+ const { files, findings } = await convert(editor.presentation, {
618
621
  format: "pdf", // "pdf" | "png" | "svg"
619
- slides: "all", // or "current" with slideIndex, or [slide numbers]; hidden slides only with includeHidden
620
- pdfMode: "vector", // or "raster" (an image per slide; also pass pdfLib: import * as pdfLib from "pdf-lib"); PNG and raster density: scale 1 to 4
622
+ slides: "1,3-5", // core's slide selection, counted from 1 (3, "1-3", [1, 3]); omitted: every slide that is not hidden (includeHidden: all)
623
+ raster: false, // PDF only: true draws each page as an image (also pass pdfLib: import * as pdfLib from "pdf-lib"); PNG and raster density: scale 1 to 4
624
+ zip: false, // PNG and SVG only: true packs the slides into one archive instead of one file per slide
625
+ name: "q3-review", // the files' base name; default the deck's filename, else its name
621
626
  fonts, // the renderer's fonts handle (loadFonts): measures, loads the faces the deck needs, supplies the faces to embed
622
- renderOptions, // catalogs, date, ...
623
- signal, onProgress, onDiagnostic,
627
+ renderOptions, // catalogs, date, ... (the renderer's ToSvgOptions)
628
+ signal, onProgress, onFinding,
624
629
  });
625
- // result.download = { name, type, bytes }: one file, or a ZIP of the slides; result.diagnostics lists what to review.
630
+ // files: [{ name, type, bytes, slide?, id?, width?, height?, pages?, slides?, entries? }]; findings: core's Finding[] to review.
626
631
  ```
627
632
 
628
- - **Same drawing as the preview.** The slides are drawn by `renderSvg` with the same fonts handle as the preview (its `textMeasurement`), so a PNG or SVG is the preview, and the PDF is converted from those SVGs rather than laid out again.
629
- - **Fonts.** The fonts handle loads the faces the deck needs before anything is drawn (a failure rejects with `fonts-unavailable`). Only faces the handle's registry holds are embedded (bundled or hash-pinned, never a system font), only where a slide draws them, as `@font-face` data in each SVG and as subsets in the PDF. A face whose own license text is not OFL, Apache, MIT or UFL is left out and reported (`export-font-license`).
630
- - **PDF** is the renderer's vector PDF (selectable text, vector shapes, embedded subsets, tagged structure), `mode: "raster"` is the image-only form. **PNG** is drawn on a canvas from the same SVG (within anti-aliasing of the renderer's resvg PNG) and is limited to 40 megapixels. **SVG** files are standalone (XML header, fonts embedded, no external references). Several files are packed in a ZIP (`createZip`, no dependency).
631
- - **Names.** `exportFileName(deck, ext, suffix)` uses the deck's `filename` (a trailing .pptx/.pdf/.png/.svg dropped), else the slugified `name`, else `presentation`; slides are `name-01.png`, archives `name-png.zip`.
633
+ - **Same drawing as the preview.** The slides are drawn by `toSvg` with the same fonts handle as the preview (its `textMeasurement`), so a PNG or SVG is the preview, and the PDF is converted from those SVGs rather than laid out again.
634
+ - **Fonts.** The fonts handle loads the faces the deck needs before anything is drawn (a failure rejects with `fonts-unavailable`). Only faces the handle's registry holds are embedded (bundled or hash-pinned, never a system font), only where a slide draws them, as `@font-face` data in each SVG and as subsets in the PDF. A face whose own license text is not OFL, Apache, MIT or UFL is left out and reported (`fonts/export-font-license`).
635
+ - **PDF** is the renderer's vector PDF (selectable text, vector shapes, embedded subsets, tagged structure), `raster: true` is the image-only form. **PNG** is drawn on a canvas from the same SVG (within anti-aliasing of the renderer's resvg PNG) and is limited to 40 megapixels. **SVG** files are standalone (XML header, fonts embedded, no external references).
636
+ - **Files.** A PDF is one file (`pages`, and the one-based `slides` it shows). PNG and SVG give one file per slide, in slide order, each with its one-based `slide`, its `id` and its `width` and `height` in pixels, as core's `convert` does; with `zip: true` they are one archive (`createZip`, no dependency; `entries` names the files in it), even for one slide. `zip` with a PDF, or `raster` with PNG or SVG, rejects with `invalid-option`.
637
+ - **Names.** `exportFileName(deck, ext, suffix)` uses the deck's `filename` (a trailing .pptx/.pdf/.png/.svg dropped), else the slugified `name`, else `presentation`; the `name` option replaces both. Slides are numbered as core numbers them (`name-001.png`, more digits for a deck of 1000 slides or more), archives are `name.zip`.
638
+ - **Slides.** `slidesToConvert(deck, { slides, includeHidden })` gives the one-based slide numbers a conversion covers. A selection names exactly its slides, hidden or not; a malformed one, or a slide past the end, rejects with core's `invalid-option` (`OPFApiError`, from `parseSlideSelection`). A deck with no slide to convert rejects with `export-no-slides`.
632
639
  - **Progress and cancel.** `onProgress({ stage, done, total, message })` reports fonts, drawing, per-page conversion and packing; an aborted `signal` rejects with `export-aborted` between pages and slides.
633
- - **Diagnostics.** `describeDiagnostic` normalises renderer and converter notes into `{ code, severity, message, slide? }`: `pdf-font-substituted`, `pdf-glyph-missing`, `pdf-raster-fallback` and the renderer's own are `warning`; `pdf-font-embedded` is `info`.
634
- - PDF and PNG need `@openpresentation/opf-render` with its `export-browser` entry (RR-23, opf-render#105); without it they reject with `export-unavailable` and SVG still works. The entry imports no PDF library (opf-render 0.16): the vector PDF, PNG and SVG need nothing else, and `pdfMode: "raster"` needs the `pdfLib` option. Verified in Chromium; Safari and Firefox are not exercised in CI. The playground bundle grows by the PDF writer, fontkit shaping and the bidi algorithm.
640
+ - **Findings.** Renderer and converter notes are core's `Finding` (finding.schema.json), as core's conversion reports them: `ruleId` `render/<code>` (the renderer), `pdf/<code>` (the PDF converter) or `fonts/export-font-license`, `category` the same word, `path` a JSON Pointer, `slide` (zero-based, as the Finding schema defines it) and `slideId`, and the note's facts (font family, weight, glyph count) in `measured`. `pdf/pdf-font-substituted`, `pdf/pdf-glyph-missing`, `pdf/pdf-raster-fallback` and the renderer's own are `warning`; `pdf/pdf-font-embedded` is `info`. A finding about a slide that is not converted is left out, and repeats are kept once.
641
+ - **Errors.** A rejection's `code` is `export-format` (no such format), `export-no-slides`, `export-aborted`, `export-unavailable`, `export-fonts-unlicensed`, `fonts-unavailable`, `invalid-option` or a renderer code (`converter-missing` for a raster PDF without `pdfLib`).
642
+ - PDF and PNG need `@openpresentation/opf-render` with its `export-browser` entry (RR-23, opf-render#105); without it they reject with `export-unavailable` and SVG still works. The entry imports no PDF library (opf-render 0.16): the vector PDF, PNG and SVG need nothing else, and `raster: true` needs the `pdfLib` option. Verified in Chromium; Safari and Firefox are not exercised in CI. The playground bundle grows by the PDF writer, fontkit shaping and the bidi algorithm.
635
643
 
636
644
  Catalog controls initialize an absent deck or slide `design` for known theme, color-scheme and font-scheme fields as one validated transaction. Undo removes a newly created parent; existing design fields are retained. DOM, React and Svelte catalog controls accept `onError(error)` and emit a bubbling `opferror` event on rejected edits, restore the committed selection and expose native validity feedback. General `editor.set` and JSON Patch require existing parents.
637
645
 
package/dist/assets.js CHANGED
@@ -115,7 +115,7 @@ export function prepareImageAsset(presentation, image, { alt, id } = {}) {
115
115
  /**
116
116
  * Add an uploaded image and use it in one undoable transaction. `build(reference, presentation)` returns the
117
117
  * prepared change that uses the image (any `prepare...` function of this package, for example
118
- * `(ref, doc) => prepareLogoVariant(doc, "light", ref)`); its patches run after the asset patch.
118
+ * `(ref, doc) => prepareOrganizationLogo(doc, "icon", ref, { background: "onDark" })`); its patches run after the asset patch.
119
119
  *
120
120
  * Options: `alt` (saved on the asset), `maxBytes`, `meta`, and `onAddAsset({ name, mediaType, bytes, size, alt, file })`
121
121
  * for a host that stores images itself: it returns the reference to use (a web address or an `asset:` id the
package/dist/canvas.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { EditorSession } from "./index.js";
2
- import type { RenderSvgOptions } from "@openpresentation/opf-render";
2
+ import type { ToSvgOptions } from "@openpresentation/opf-render";
3
3
  import type { BrowserFontsHandle } from "@openpresentation/opf-render/fonts-browser";
4
4
  import type { SlideComposition } from "@openpresentation/opf/composition";
5
5
  import type { Catalog } from "@openpresentation/opf";
@@ -15,7 +15,7 @@ export interface FontsReadyHandlers {
15
15
  ready?: () => void;
16
16
  failed?: (error: Error) => void;
17
17
  /** The options the host renders with (`catalogs`, ...), passed to the fonts handle. */
18
- renderOptions?: RenderSvgOptions;
18
+ renderOptions?: ToSvgOptions;
19
19
  }
20
20
  export declare const FONTS_PENDING: "fonts-pending";
21
21
  export declare const FONTS_UNAVAILABLE: "fonts-unavailable";
@@ -41,7 +41,7 @@ export interface CanvasEditorOptions {
41
41
  /** Show keyboard-accessible dividers for resizing composition tracks. */
42
42
  layoutEditing?: boolean;
43
43
  /** Render options. The canvas draws the document as authored (`variables: false`: a template's `{{tokens}}` stay visible, so inline edits never overwrite them); pass `variables` to draw resolved values instead. */
44
- renderOptions?: RenderSvgOptions;
44
+ renderOptions?: ToSvgOptions;
45
45
  /**
46
46
  * The renderer's fonts handle (`loadFonts()` from `@openpresentation/opf-render/fonts-browser`). Its `textMeasurement` lays the slide out and
47
47
  * its faces draw it. With its `pending` and `ensure` the canvas never renders a document whose faces are still loading: it shows
@@ -117,7 +117,7 @@ export interface CanvasEditor {
117
117
  * notes, deck fields) or the slide is not drawn yet (fonts loading). Moves keyboard focus only with `focus: true`.
118
118
  */
119
119
  reveal(path: string, options?: { focus?: boolean }): string | null;
120
- setRenderOptions(options: RenderSvgOptions): boolean;
120
+ setRenderOptions(options: ToSvgOptions): boolean;
121
121
  destroy(): void;
122
122
  }
123
123
  export declare function allocatedSelectionBox(
package/dist/canvas.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  splitOpfPath,
8
8
  } from "./index.js";
9
9
  import {
10
- renderSlideSvg,
10
+ toSvg,
11
11
  resolvePresentation,
12
12
  } from "@openpresentation/opf-render/svg";
13
13
  import {
@@ -77,7 +77,7 @@ export function createCanvasEditor(container, options = {}) {
77
77
  const resolveOptions = () => ({ ...renderOptions, catalogs: mergeCatalogs(editor.catalogs, renderOptions.catalogs) });
78
78
  // RR-61: the handle measures, and the canvas embeds no face: the browser handle already added its faces to the page, so
79
79
  // `@font-face` data would repeat megabytes of base64 in every redraw. Exports keep the default and carry the faces they draw.
80
- const drawOptions = (extra) => ({ ...resolveOptions(), fonts: handle, embedFonts: false, ...extra });
80
+ const drawOptions = (extra) => ({ ...resolveOptions(), fonts: handle, text: "system", ...extra });
81
81
  let slideIndex = options.slideIndex ?? 0,
82
82
  // RR-32: the canvas edits the document as authored, so a template's {{tokens}} stay visible and an inline edit never
83
83
  // overwrites one with its resolved text. The Fill template panel previews the resolved deck. Pass `variables` to override.
@@ -197,7 +197,7 @@ export function createCanvasEditor(container, options = {}) {
197
197
  }
198
198
  function validateRender(presentation) {
199
199
  requireFonts(presentation);
200
- return renderSlideSvg(presentation, slideIndex, drawOptions());
200
+ return toSvg(presentation, slideIndex + 1, drawOptions());
201
201
  }
202
202
  function fontsState(kind, detail) {
203
203
  fontsShown = true;
@@ -253,7 +253,7 @@ export function createCanvasEditor(container, options = {}) {
253
253
  showToken++;
254
254
  const slides = presentation.slides ?? [];
255
255
  slideIndex = Math.max(0, Math.min(slideIndex, slides.length - 1));
256
- const svgText = renderSlideSvg(presentation, slideIndex, drawOptions({ trace: true }));
256
+ const svgText = toSvg(presentation, slideIndex + 1, drawOptions({ trace: true }));
257
257
  preview.innerHTML = svgText;
258
258
  const svg = preview.querySelector("svg");
259
259
  svg.setAttribute("role", "group");
package/dist/data.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export {parseTabularData, importData, OPFDataImportError} from "@openpresentation/opf/data";
2
- export type {TabularData, DataImportOptions, ImportDataOptions} from "@openpresentation/opf/data";
1
+ export {parseTabularData, ingest, OPFDataImportError} from "@openpresentation/opf/data";
2
+ export type {TabularData, DataImportOptions, IngestOptions} from "@openpresentation/opf/data";
3
3
  import type { JsonPatchOperation } from "./index.js";
4
4
 
5
5
  /** The id pattern of a top-level dataset (the assets id pattern). */
@@ -19,5 +19,5 @@ export interface DatasetImport {
19
19
  /** True when a dataset of that id existed and its columns and rows were replaced. */
20
20
  replaced: boolean;
21
21
  }
22
- /** Store imported data (the result of `importData`) as a shared dataset, as `opf import-data --dataset <id>` does. Throws `dataset-id-invalid`. */
22
+ /** Store imported data (the result of `ingest`) as a shared dataset, as `opf ingest --dataset <id>` does. Throws `dataset-id-invalid`. */
23
23
  export declare function prepareDatasetImport(presentation: unknown, content: { table: unknown } | { chart: { type: string; data: unknown } }, options: DatasetImportOptions): DatasetImport;
package/dist/data.js CHANGED
@@ -1,4 +1,4 @@
1
- export {parseTabularData, importData, OPFDataImportError} from "@openpresentation/opf/data";
1
+ export {parseTabularData, ingest, OPFDataImportError} from "@openpresentation/opf/data";
2
2
  import { opfPathToJsonPointer } from "./index.js";
3
3
  import { fail } from "./edit-helpers.js";
4
4
 
@@ -8,7 +8,7 @@ export const DATASET_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
8
8
  const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
9
 
10
10
  /**
11
- * Store imported data as a shared dataset (RR-54), as `opf import-data --dataset <id>` does. `content` is what `importData` returns
11
+ * Store imported data as a shared dataset (RR-54), as `opf ingest --dataset <id>` does. `content` is what `ingest` returns
12
12
  * (`{ table }` or `{ chart: { type, data } }`). Returns `{ id, content, patches, replaced }`: `content` is the same table or chart holding
13
13
  * `{ dataset: id }` instead of the rows, and `patches` write `datasets.<id>` (the imported columns and rows replace those of an existing
14
14
  * dataset of that id, which keeps its title, description and source; `source` is merged into its `source`). Apply `patches` and the patch that
@@ -18,7 +18,7 @@ export function prepareDatasetImport(presentation, content, options = {}) {
18
18
  const id = options.id;
19
19
  if (typeof id !== "string" || !DATASET_ID_PATTERN.test(id)) throw fail("dataset-id-invalid", 'A dataset id starts with a letter or digit and holds letters, digits, ".", "_" or "-".', { id });
20
20
  const isTable = isObject(content) && isObject(content.table);
21
- if (!isTable && !(isObject(content) && isObject(content.chart) && isObject(content.chart.data))) throw fail("dataset-content-invalid", "Give the table or chart that importData returned.", {});
21
+ if (!isTable && !(isObject(content) && isObject(content.chart) && isObject(content.chart.data))) throw fail("dataset-content-invalid", "Give the table or chart that ingest returned.", {});
22
22
  const data = isTable ? content.table : content.chart.data;
23
23
  const datasets = isObject(presentation) && isObject(presentation.datasets) ? presentation.datasets : undefined;
24
24
  const previous = datasets && Object.hasOwn(datasets, id) && isObject(datasets[id]) ? datasets[id] : undefined;