@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 +50 -42
- package/dist/assets.js +1 -1
- package/dist/canvas.d.ts +4 -4
- package/dist/canvas.js +4 -4
- package/dist/data.d.ts +3 -3
- package/dist/data.js +3 -3
- package/dist/design-controls.js +157 -44
- package/dist/design-options.d.ts +78 -22
- package/dist/design-options.js +240 -110
- package/dist/export.d.ts +42 -41
- package/dist/export.js +151 -87
- package/dist/font-gate.js +1 -1
- package/dist/review.d.ts +6 -1
- package/dist/review.js +37 -1
- package/dist/template-panel.d.ts +1 -1
- package/dist/template-panel.js +7 -4
- package/dist/templates.js +1 -1
- package/package.json +6 -5
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
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
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
|
|
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 `
|
|
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, `
|
|
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 `
|
|
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 {
|
|
192
|
+
import { toSvg } from "@openpresentation/opf-render";
|
|
193
193
|
|
|
194
194
|
const editor = createEditorSession(opfDocument, { rejectInvalid: true });
|
|
195
|
-
const svg =
|
|
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,
|
|
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
|
-
|
|
344
|
-
|
|
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
|
|
358
|
-
|
|
|
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`, `
|
|
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
|
|
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 {
|
|
373
|
+
import { prepareOrganizationLogo } from "@openpresentation/opf-editor/design-options";
|
|
371
374
|
|
|
372
|
-
const change = await applyImageUpload(editor, file, (reference, presentation) =>
|
|
373
|
-
change.assetId; // "acme-logo": the document now has assets["acme-logo"] = { src: "data:image/png;base64,...", mediaType, title, alt } and
|
|
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 (
|
|
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) =>
|
|
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,
|
|
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,
|
|
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
|
|
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 (
|
|
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 {
|
|
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 }) =>
|
|
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
|
|
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 {
|
|
617
|
-
const
|
|
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: "
|
|
620
|
-
|
|
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,
|
|
627
|
+
renderOptions, // catalogs, date, ... (the renderer's ToSvgOptions)
|
|
628
|
+
signal, onProgress, onFinding,
|
|
624
629
|
});
|
|
625
|
-
//
|
|
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 `
|
|
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), `
|
|
631
|
-
- **
|
|
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
|
-
- **
|
|
634
|
-
-
|
|
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) =>
|
|
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 {
|
|
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?:
|
|
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?:
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
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,
|
|
2
|
-
export type {TabularData, DataImportOptions,
|
|
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 `
|
|
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,
|
|
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
|
|
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
|
|
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;
|