@openpresentation/opf-editor 0.25.2 → 0.26.0-rc.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 +27 -25
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/json-editor.d.ts +10 -0
- package/dist/json-editor.js +17 -5
- package/dist/source-pane.d.ts +12 -5
- package/dist/source-pane.js +71 -11
- package/package.json +58 -58
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs
|
|
|
9
9
|
| You use | Also install |
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| The canvas, the preview, SVG downloads in a browser (the host serves its own font files) | nothing |
|
|
12
|
-
| `loadFonts` from `/fonts-node` (a server, a test, a build script such as `
|
|
12
|
+
| `loadFonts` from `/fonts-node` (a server, a test, a build script such as `pnpm 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
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
14
|
| `convert` PDF (vector), PNG, SVG | nothing: the browser entry `@openpresentation/opf-render/export-browser` imports no converter |
|
|
15
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` |
|
|
@@ -82,7 +82,7 @@ Click a categorical property/value or press `Ctrl+Space`, `Cmd+Space` or `Alt+Do
|
|
|
82
82
|
|
|
83
83
|
In OPF 0.19, layout choices change only the template reference and preserve authored content. Empty regions remain available for explicit insertion; choosing a template does not invent blank payloads. The JSON field menu groups templates by intent and describes their supported regions. Hosts apply `replaceFieldOption` or `fieldOptionEdit(context, value)` as one undoable edit and check that `context.source` is still current.
|
|
84
84
|
|
|
85
|
-
Run `
|
|
85
|
+
Run `pnpm run test:json` and `pnpm run test:json-browser` for the focused model and offline browser checks. The browser runner also accepts a report path followed by a clean installed consumer directory. These checks cover the JSON control; renderer, native PowerPoint and production adoption remain separate acceptance steps.
|
|
86
86
|
|
|
87
87
|
Version 0.7.0 uses core 0.10.0 and renderer 0.8.0. Code source/metadata edits preserve line endings, literal tabs and cancellation/undo through accepted trace targets. Committed preview uses shared quote/code geometry; the active source textarea uses native browser editing. `paginateSlide` records a one-page readability-policy change in undo history, and an unchanged repeat is a no-op. Coordinated examples use PPTX 0.8.0 for exact code source/metadata recovery. Native formatting, font theme and geometry are not restored; quotes still import as editable text blocks with structure and readability-policy loss.
|
|
88
88
|
|
|
@@ -110,9 +110,10 @@ const pane = createSourcePane(container, {
|
|
|
110
110
|
pane.setFormat('yaml'); pane.focusPath('/slides/1/title'); pane.getText(); pane.destroy();
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Methods: `replaceDocument(document, { source?, name?, meta? })` (below), `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format; exact for JSON, best effort for YAML and Markdown), `getText()`, `refresh()`, `setName`, `setDensity`, `setReadOnly` and `destroy()`. A finding is `{ path, message, severity, line? }` (core `Finding` objects fit). Rows show the path in mono and the line at the right; a click moves the cursor and calls `onFindingSelect`.
|
|
113
|
+
Methods: `replaceDocument(document, { source?, format?, name?, meta? })` (below), `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format; exact for JSON, best effort for YAML and Markdown), `getText()`, `refresh()`, `setName`, `setDensity`, `setReadOnly` and `destroy()`. A finding is `{ path, message, severity, line? }` (core `Finding` objects fit). Rows show the path in mono and the line at the right; a click moves the cursor and calls `onFindingSelect`.
|
|
114
114
|
|
|
115
|
-
- **Opening a document: `pane.replaceDocument(document, { source, name })`.** Loading an example or a file into the pane (never by writing its text into the code editor, which the pane applies as a typed edit and forks the catalog records the two documents share) calls `editor.replaceDocument`: one undoable change, no catalog record forked. `document` is a parsed document or JSON text; `source` is the
|
|
115
|
+
- **Opening a document: `pane.replaceDocument(document, { source, format, name })`.** Loading an example or a file into the pane (never by writing its text into the code editor, which the pane applies as a typed edit and forks the catalog records the two documents share) calls `editor.replaceDocument`: one undoable change, no catalog record forked. `document` is a parsed document or JSON text; `name` is the file name. `source` is the text the document was read from, in any of the three formats: the pane shows it as written (the JSON's formatting, the YAML's comments, the Markdown's layout) when the pane is in that format, and core's rewrite of the document otherwise. `format` (`"json"`, `"yaml"` or `"md"`) says which format `source` is; left out, it is inferred as the first of JSON, the pane's format, YAML and Markdown that reads as exactly this document (a `source` that reads as no format, or as another document, is ignored; an unknown `format` throws a `TypeError` before anything changes). The original text of a format other than the one shown is kept too: switching the pane to it shows those bytes, not a rewrite. Undo and Redo bring back each document's text exactly in the format shown (the opened text as written, the replaced document's text as it was, including YAML comments), so one Undo returns to the document and the text the visitor had. The pane keeps the last 8 such texts, which are also what a later load of the same document shows. A refused document throws (`invalid-opf-edit`, with `issues`) and changes nothing, and works in a read-only pane. A diff typed or pasted into the pane is still an edit, and editing a catalog record in it forks the record.
|
|
116
|
+
- **Keyboard Undo and Redo in the pane (one model).** Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z (Ctrl+Y) work in two layers, in this order. First CodeMirror's history, which holds only the text typed in the pane since its last whole-document change (a `replaceDocument`, or a session Undo, Redo or restore, each of which clears it because the typed edits no longer fit the text). When that has nothing left, the key steps the session over a whole-document replace, exactly as its Undo and Redo do, so the text comes back byte for byte: Undo steps back over the nearest `replaceDocument` together with the pane's own edits typed on it (the typing was already undone, step by step, by CodeMirror's history); Redo steps forward over the replace. The key never reaches over an edit made elsewhere (the canvas, a panel): Undo stops at it, and the host's Undo button (or `editor.undo()`) undoes that edit, after which the key reaches the load again. A read-only pane takes neither key. `mountJsonCodeEditor` exposes the same seam as `historyFallback: { undo(), redo() }` (answer `true` when you did something) plus `resetHistory()` and `undoDepth()` on the control.
|
|
116
117
|
- **JSON is canonical** and keeps the author's bytes: a YAML or Markdown edit rewrites only the changed JSON tokens (`exact-source`), and Undo restores the exact spelling.
|
|
117
118
|
- **YAML and Markdown** are core's browser-safe converters, `@openpresentation/opf/yaml` and `/markdown`. Switching format writes the session's document in the new format. An edit parses back to the document and reaches the session as one `replace` patch, after a short delay (`applyDelay`, default 250 ms) so one burst of typing is one undo step. A syntax error, or an OPF rule the session refuses, is listed with its line and never reaches the document; Undo, Redo and outside edits replace a draft that did not parse.
|
|
118
119
|
- **Nothing is lost silently.** Markdown writes what its dialect has no syntax for (a `design`, region keys, nested groups) as YAML in `opf-slide` and `opf-block` fences; each such part is a warning finding with its JSON Pointer and reason. Comments in YAML cannot survive a rewrite from the document: Format and outside edits drop them and say how many. A document that is not valid OPF cannot be written as YAML or Markdown; the pane stays in JSON and says so.
|
|
@@ -121,7 +122,7 @@ Methods: `replaceDocument(document, { source?, name?, meta? })` (below), `setFin
|
|
|
121
122
|
- **Styles.** `src/styles/source-pane.css` uses only `--opf-ui-*` tokens under the `opf-source-` prefix. It ships in the editor's `styles.css` (`@openpresentation/opf-editor/styles.css`), which also defines the tokens.
|
|
122
123
|
- **Dependency.** `@codemirror/lang-yaml` (MIT) highlights YAML; Markdown uses a small stream tokenizer built on `@codemirror/language`, so no Markdown, HTML, CSS or JavaScript grammar is added.
|
|
123
124
|
|
|
124
|
-
Icons are inline paths from [Lucide](https://lucide.dev) (ISC license). The harness is `examples/source-pane.html` (built by `
|
|
125
|
+
Icons are inline paths from [Lucide](https://lucide.dev) (ISC license). The harness is `examples/source-pane.html` (built by `pnpm run build:playground`); `pnpm run test:source-pane-browser` drives it.
|
|
125
126
|
|
|
126
127
|
## Layout editor (RR-99)
|
|
127
128
|
|
|
@@ -182,12 +183,12 @@ createLayoutInspector(inspectorHost, { model });
|
|
|
182
183
|
|
|
183
184
|
**Keyboard:** arrows move between siblings, Enter goes into a split and Esc goes up; `[` and `]` move a region earlier or later (at the end of a split it leaves the split); Shift with a bracket moves it into the neighboring split; Cmd or Ctrl+D duplicates; Delete deletes. A divider is `role="separator"` with `aria-valuenow` (the first side's share of the pair, in percent) and `aria-valuetext` ("2 : 1"); Arrow keys resize it by half a share (Shift: one), Enter makes the sides equal. The layers panel is `role="tree"` (Up and Down, Right to open or enter, Left to close or go to the parent, Home, End, F2). Every action is also a button in the inspector. Touch density makes controls 40px and the divider hit area wider; below 760px the palette is a row above the canvas and Layers and Inspector are tabs under it.
|
|
184
185
|
|
|
185
|
-
The headless operations the editor runs are in `@openpresentation/opf-editor/layout-tree` (`splitRegion`, `wrapNodes`, `unwrapNode`, `moveNodes`, `deleteNodes`, `duplicateNodes`, `resizeSizes`, `layoutBoxes` and more): pure functions that return a new record and the paths to select, or throw a `LayoutOperationError` with the reason, and keep a core-valid record core-valid. `
|
|
186
|
+
The headless operations the editor runs are in `@openpresentation/opf-editor/layout-tree` (`splitRegion`, `wrapNodes`, `unwrapNode`, `moveNodes`, `deleteNodes`, `duplicateNodes`, `resizeSizes`, `layoutBoxes` and more): pure functions that return a new record and the paths to select, or throw a `LayoutOperationError` with the reason, and keep a core-valid record core-valid. `pnpm run test:layout-editor-browser` drives `examples/layout-editor.html` (built by `pnpm run build:playground`); the playground's Layouts view mounts the same editor on the presentation's custom layouts. Icons are inline paths from [Lucide](https://lucide.dev) (ISC license).
|
|
186
187
|
|
|
187
188
|
## Scope
|
|
188
189
|
|
|
189
190
|
- Package: `@openpresentation/opf-editor`
|
|
190
|
-
- Repository: `OpenPresentation/opf
|
|
191
|
+
- Repository: `OpenPresentation/opf` (`packages/editor`)
|
|
191
192
|
- License: MIT
|
|
192
193
|
- Compatibility target: `@openpresentation/opf`
|
|
193
194
|
- Renderer relationship: built on `@openpresentation/opf-render` trace output
|
|
@@ -224,7 +225,7 @@ await canvas.ready;
|
|
|
224
225
|
// canvas.destroy() when unmounting.
|
|
225
226
|
```
|
|
226
227
|
|
|
227
|
-
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 `
|
|
228
|
+
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 `pnpm 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.
|
|
228
229
|
|
|
229
230
|
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.
|
|
230
231
|
|
|
@@ -290,11 +291,12 @@ The package runtime must stay local and deterministic:
|
|
|
290
291
|
## Development
|
|
291
292
|
|
|
292
293
|
```sh
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
pnpm install --frozen-lockfile # from the repository root (the OPF monorepo)
|
|
295
|
+
pnpm run build:deps # gallery and core, which the editor build and tests import
|
|
296
|
+
pnpm run build
|
|
297
|
+
pnpm run typecheck
|
|
298
|
+
pnpm test
|
|
299
|
+
pnpm run validate
|
|
298
300
|
```
|
|
299
301
|
|
|
300
302
|
## Headless Editing
|
|
@@ -671,7 +673,7 @@ useEffect(() => {
|
|
|
671
673
|
|
|
672
674
|
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.
|
|
673
675
|
|
|
674
|
-
Run `
|
|
676
|
+
Run `pnpm run test:design-controls-browser` (after `pnpm run build:playground`) for the real-browser check of every control, its keyboard operation and its undo.
|
|
675
677
|
|
|
676
678
|
### What a switch does not establish
|
|
677
679
|
|
|
@@ -749,7 +751,7 @@ The editor panels ship one stylesheet. Import it once, next to your own CSS:
|
|
|
749
751
|
import "@openpresentation/opf-editor/styles.css";
|
|
750
752
|
```
|
|
751
753
|
|
|
752
|
-
`
|
|
754
|
+
`pnpm run build` writes `dist/styles.css`: the token layer (`src/styles/tokens.css`) first, then every other `src/styles/*.css` in alphabetical order. It is plain CSS with no preprocessor, no `@import` and no `url()`: nothing is fetched. The package bundles no fonts either; load Inter and Geist Mono yourself (or leave the fallbacks: `system-ui` and `ui-monospace`). The panels that existed before it (design controls, review, slide manager, find, fill template, numbering, chart options, data grid, crop, the custom layout editor, the JSON editor and the restore banner) are styled with the tokens and nothing else. The find, review, crop and data-grid components still inject a small fallback stylesheet of their own, so they look as before when you do not import `styles.css`; with it, its rules win whichever `<style>` comes last.
|
|
753
755
|
|
|
754
756
|
**Tokens.** Every rule reads only `--opf-ui-*` custom properties, defined on `:root` and on `.opf-ui`, light by default. Override any of them on `:root`, on a wrapper with the class `opf-ui`, or on one component's container:
|
|
755
757
|
|
|
@@ -836,7 +838,7 @@ const { files, findings } = await convert(editor.presentation, {
|
|
|
836
838
|
});
|
|
837
839
|
```
|
|
838
840
|
|
|
839
|
-
`convert` reads `fonts.subsets` and `fonts.outlines` from that handle, so nothing else changes: the same call writes SVG files with the faces cut to what each slide draws (the licence rule above still decides which faces may be embedded, and the renderer cuts only faces whose licence allows a modified version; the others stay whole) and, with `renderOptions: { text: "paths" }`, SVG and PNG files whose text is glyph outlines (no font needed). A PDF is unaffected: it embeds the registry's faces and cuts its own subsets, and it always draws selectable text. `renderOptions.subsetFonts: false` embeds whole faces. The canvas draws with `text: "system"`, which embeds nothing, so it never reads either. The playground does this: `
|
|
841
|
+
`convert` reads `fonts.subsets` and `fonts.outlines` from that handle, so nothing else changes: the same call writes SVG files with the faces cut to what each slide draws (the licence rule above still decides which faces may be embedded, and the renderer cuts only faces whose licence allows a modified version; the others stay whole) and, with `renderOptions: { text: "paths" }`, SVG and PNG files whose text is glyph outlines (no font needed). A PDF is unaffected: it embeds the registry's faces and cuts its own subsets, and it always draws selectable text. `renderOptions.subsetFonts: false` embeds whole faces. The canvas draws with `text: "system"`, which embeds nothing, so it never reads either. The playground does this: `pnpm run build:playground` copies both files next to the page.
|
|
840
842
|
- **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`).
|
|
841
843
|
- **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).
|
|
842
844
|
- **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`.
|
|
@@ -926,7 +928,7 @@ bindPresentShortcut(document, () => presenter.start(current)); // F5
|
|
|
926
928
|
|
|
927
929
|
`createPresentButton(container, { onPresent, disabled, density, variant })` mounts an icon-only button (the Lucide Presentation icon, inlined) named "Present" (`aria-label`), titled "Present (F5)". `variant` is `"quiet"` (the default: a transparent icon in `--opf-ui-muted-fg` that turns `--opf-ui-fg` on `--opf-ui-muted` on hover, like the toolbar's undo and redo, so a lone icon beside a primary Export does not compete with it) or `"primary"` (the filled accent look of the Export menu's trigger). `disabled` is `true` or a reason string (the title); the button stays focusable (`aria-disabled`) and does nothing. It returns `{ element, setDisabled(reason), setDensity(density), setVariant(variant), destroy() }`. `bindPresentShortcut(target, onPresent)` binds F5 (and prevents the browser's reload); it ignores F5 while typing in inputs, selects and editors (CodeMirror, contenteditable), in menus and dialogs, and with a modifier key, and returns a function that removes the listener. `examples/playground.js` wires all three next to the Export menu.
|
|
928
930
|
|
|
929
|
-
Tests: `
|
|
931
|
+
Tests: `pnpm run test:presenter-browser` and `pnpm run test:present-button-browser`.
|
|
930
932
|
|
|
931
933
|
## Opening a deck file (RR-98)
|
|
932
934
|
|
|
@@ -991,17 +993,17 @@ Svelte bindings are isolated under `@openpresentation/opf-editor/svelte` as acti
|
|
|
991
993
|
|
|
992
994
|
## Release Lane
|
|
993
995
|
|
|
994
|
-
|
|
996
|
+
The editor releases with the other five packages of the OPF monorepo at one version, through the repository's release workflow (npm Trusted Publishing from GitHub Actions OIDC, with npm provenance; no npm token is stored). The owner authorized agents to prepare and publish npm releases whenever a release is required (2026-09-29); this authorization does not waive any gate. The steps, the gates and the order are in the root `AGENTS.md` and `docs/release-process.md`. In short:
|
|
995
997
|
|
|
996
|
-
1.
|
|
997
|
-
2.
|
|
998
|
-
3. Verify with `npm view @openpresentation/opf-editor@<version> version gitHead dist.attestations
|
|
998
|
+
1. A release-prep PR contains only the version bump (all six manifests), `CHANGELOG.md` files assembled from the root `changes/` fragments, dependency ranges, the lockfile and current-instruction docs.
|
|
999
|
+
2. The release workflow rebuilds `dist/` from source (it is not tracked), runs the gates on the release commit and publishes with provenance. Never republish an existing version.
|
|
1000
|
+
3. Verify with `npm view @openpresentation/opf-editor@<version> version gitHead dist.attestations`.
|
|
999
1001
|
|
|
1000
1002
|
## Shared dynamic composition
|
|
1001
1003
|
|
|
1002
|
-
The current checkout uses `@openpresentation/opf/composition` for portable geometry. Slides can select `auto`, `row`, `column`, or `grid`, set weighted tracks, and request path-specific overflow diagnostics. See
|
|
1004
|
+
The current checkout uses `@openpresentation/opf/composition` for portable geometry. Slides can select `auto`, `row`, `column`, or `grid`, set weighted tracks, and request path-specific overflow diagnostics. See `docs/dynamic-composition.md` at the repository root for the complete contract.
|
|
1003
1005
|
|
|
1004
|
-
The
|
|
1006
|
+
The editor requires the core, renderer and PPTX converter of its own release (`workspace:^` in this repository, `^<version>` in the published package). In the workspace the packages link to each other, so `pnpm run test:contract` here checks shared geometry and import/export behavior against this checkout's sources.
|
|
1005
1007
|
|
|
1006
1008
|
## Local interactive demo
|
|
1007
1009
|
|
|
@@ -1026,7 +1028,7 @@ The playground includes Copy OPF and Import dialogs with paste, file/drop, galle
|
|
|
1026
1028
|
|
|
1027
1029
|
The **PowerPoint** button commits the current canvas draft and prepares an editable `.pptx` download from that snapshot, using the preview's text measurements. Conversion diagnostics remain visible in the download dialog. Missing/remote images require an explicit host resolver or embedded data; export does not silently fetch them or omit them. Save OPF separately to retain the complete original source. Since 0.14, PPTX export names the font the user selected, so PowerPoint uses it when installed or available as a Microsoft 365 cloud font. The download does not embed font binaries, and license-restricted (proprietary) fonts are never embedded. Arbitrary native positions and unsupported PowerPoint features can change during reimport.
|
|
1028
1030
|
|
|
1029
|
-
Run `
|
|
1031
|
+
Run `pnpm install` at the repository root, then `pnpm run build:deps` and `pnpm run build:playground` here, then serve `artifacts/playground` with a local static server. This example uses the workspace's core/render/PPTX packages and this checkout's editor source; it does not require an account, AI provider or paid service. `pnpm run test:playground` builds and executes a real browser flow (local Edge on Windows, installed Chromium in CI): author JSON, inline edit, export a native merged table, validate/reimport the actual downloaded file, undo/redo, save OPF and reject malformed input without changing the document. It is browser behavior evidence, not a claim of native PowerPoint raster equivalence.
|
|
1030
1032
|
|
|
1031
1033
|
Slide insertion does not merge root speakers, organizations, or narrative metadata. Use Open as a presentation for the full source document. Some gallery presets contain minimal examples. The root OPF workspace's `docs/live-editor.md` documents the complete demo and npm API workflow.
|
|
1032
1034
|
|
|
@@ -1046,7 +1048,7 @@ Select rendered rich text to format it, or click a rich text block to type (see
|
|
|
1046
1048
|
|
|
1047
1049
|
Version 0.7.0: rich input maps the textarea's LF-normalized offsets back to the original source. Typing preserves untouched CRLF/CR endings and surrounding run metadata; newly inserted newlines use the first source line-ending style. The `updateRichTextInput` change offsets are native textarea offsets, while formatting and replacement helpers continue to use original source offsets.
|
|
1048
1050
|
|
|
1049
|
-
Browser regression checks run with `
|
|
1051
|
+
Browser regression checks run with `pnpm run test:rich-input-browser artifacts/rich-input-browser.json measured` (or `estimated`). An optional consumer path after the mode selects an existing coordinated installed consumer; all browser runtime and font-preparation imports must then resolve inside that consumer. See [line-ending acceptance](docs/evidence/rich-input-line-endings-20260915/README.md) for scope and preserved failures.
|
|
1050
1052
|
|
|
1051
1053
|
Headless applications and agents can import `formatRichTextRange`, `replaceRichTextRange`, and `richTextContent` from `@openpresentation/opf-editor/rich-text`. The helpers preserve surrounding run metadata and accept UTF-16 offsets on whole grapheme boundaries. A null format value removes an override. Apply the returned value through the editor session or validate the resulting document before saving.
|
|
1052
1054
|
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export interface EditorDiagnosticOptions {
|
|
|
16
16
|
export declare const packageName = "@openpresentation/opf-editor";
|
|
17
17
|
|
|
18
18
|
export declare const releaseLane: Readonly<{
|
|
19
|
-
githubRepository: "OpenPresentation/opf
|
|
19
|
+
githubRepository: "OpenPresentation/opf";
|
|
20
|
+
directory: "packages/editor";
|
|
20
21
|
npmPackage: "@openpresentation/opf-editor";
|
|
21
22
|
compatibilityPackage: "@openpresentation/opf";
|
|
22
23
|
rendererPackage: "@openpresentation/opf-render";
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,8 @@ function slideContext(presentation, slideIndex, catalogs, { fonts, catalogs: _ex
|
|
|
45
45
|
export const packageName = "@openpresentation/opf-editor";
|
|
46
46
|
|
|
47
47
|
export const releaseLane = Object.freeze({
|
|
48
|
-
githubRepository: "OpenPresentation/opf
|
|
48
|
+
githubRepository: "OpenPresentation/opf",
|
|
49
|
+
directory: "packages/editor",
|
|
49
50
|
npmPackage: "@openpresentation/opf-editor",
|
|
50
51
|
compatibilityPackage: "@openpresentation/opf",
|
|
51
52
|
rendererPackage: "@openpresentation/opf-render"
|
package/dist/json-editor.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface JsonCodeEditorOptions {
|
|
|
28
28
|
catalogs?: JsonCatalogContext;
|
|
29
29
|
/** Overrides the built-in menu, allowing an existing host UI to keep its appearance. */
|
|
30
30
|
onOptions?(left?: number, top?: number): void;
|
|
31
|
+
/**
|
|
32
|
+
* Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z (Ctrl+Y) run CodeMirror's history first. When it has nothing left to undo or redo, the fallback runs
|
|
33
|
+
* and answers true when it did something (false lets the key pass on). The source pane uses it to step the session back over a
|
|
34
|
+
* whole-document replace.
|
|
35
|
+
*/
|
|
36
|
+
historyFallback?: { undo?(): boolean; redo?(): boolean };
|
|
31
37
|
onFocus?(): void;
|
|
32
38
|
onHoverField?(path: string | null): void;
|
|
33
39
|
onError?(error: unknown): void;
|
|
@@ -39,6 +45,10 @@ export interface JsonCodeEditor {
|
|
|
39
45
|
setCatalogs(catalogs: JsonCatalogContext): void;
|
|
40
46
|
setLanguage(language: "json" | "yaml" | "markdown"): void;
|
|
41
47
|
setReadOnly(readOnly: boolean): void;
|
|
48
|
+
/** Forget the typed edits CodeMirror can undo, for when the text they were typed against was replaced. The text is unchanged. */
|
|
49
|
+
resetHistory(): void;
|
|
50
|
+
/** How many typed edit groups CodeMirror can still undo. */
|
|
51
|
+
undoDepth(): number;
|
|
42
52
|
/** The CodeMirror `EditorView`. */
|
|
43
53
|
readonly view: import("@codemirror/view").EditorView;
|
|
44
54
|
openOptions(left?: number, top?: number): Promise<void>;
|
package/dist/json-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Annotation, Compartment, EditorSelection, EditorState, Prec, StateEffect, StateField, Transaction } from "@codemirror/state";
|
|
2
2
|
import { Decoration, EditorView, drawSelection, keymap, lineNumbers } from "@codemirror/view";
|
|
3
|
-
import { defaultKeymap, history, historyKeymap, indentWithTab, isolateHistory, redo } from "@codemirror/commands";
|
|
3
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab, isolateHistory, redo, undo, undoDepth } from "@codemirror/commands";
|
|
4
4
|
import { HighlightStyle, StreamLanguage, bracketMatching, codeFolding, foldKeymap, indentOnInput, indentUnit, syntaxHighlighting } from "@codemirror/language";
|
|
5
5
|
import { json, jsonParseLinter } from "@codemirror/lang-json";
|
|
6
6
|
import { yaml } from "@codemirror/lang-yaml";
|
|
@@ -167,7 +167,7 @@ function skipDelimiter(view, delimiter) {
|
|
|
167
167
|
return true;
|
|
168
168
|
}
|
|
169
169
|
function mountJsonCodeEditor(parent, options) {
|
|
170
|
-
const attributes = new Compartment(), indent = new Compartment(), language = new Compartment(), editable = new Compartment();
|
|
170
|
+
const attributes = new Compartment(), indent = new Compartment(), language = new Compartment(), editable = new Compartment(), historyField = new Compartment();
|
|
171
171
|
const editableExtension = (readOnly) => [EditorState.readOnly.of(Boolean(readOnly)), EditorView.editable.of(!readOnly)];
|
|
172
172
|
let languageName = options.language ?? "json";
|
|
173
173
|
let disposed=false, menu=null, catalogs=options.catalogs??[], generation=0;
|
|
@@ -196,6 +196,11 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
196
196
|
label: option.label, detail: option.detail, info: option.info, boost: option.boost, type: found.kind === "key" ? "property" : "enum", apply: completionApply(found, option),
|
|
197
197
|
})) };
|
|
198
198
|
}
|
|
199
|
+
// Undo and Redo run CodeMirror's history first. When it has nothing left, the host's fallback may step further (the source pane
|
|
200
|
+
// routes a whole-document replace to the session); a fallback answers true when it did something.
|
|
201
|
+
const undoCommand = (target) => undo(target) || Boolean(options.historyFallback?.undo?.());
|
|
202
|
+
const redoCommand = (target) => redo(target) || Boolean(options.historyFallback?.redo?.());
|
|
203
|
+
const historyBindings = historyKeymap.map((binding) => binding.run === undo ? { ...binding, run: undoCommand } : binding.run === redo ? { ...binding, run: redoCommand } : binding);
|
|
199
204
|
const completionExtension = options.completions ? [autocompletion({ override: [completionSource], defaultKeymap: false, icons: false, maxRenderedOptions: 60 }), completionKeys] : null;
|
|
200
205
|
let currentIndent = indentation(options.code);
|
|
201
206
|
const contentAttributes = (invalid = options.invalid, describedBy = options.describedBy) => EditorView.contentAttributes.of({
|
|
@@ -213,7 +218,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
213
218
|
jsonSource.init(()=>options.code),jsonSourceHistory,
|
|
214
219
|
indent.of(indentUnit.of(currentIndent)),
|
|
215
220
|
indentOnInput(),
|
|
216
|
-
history(),
|
|
221
|
+
historyField.of(history()),
|
|
217
222
|
drawSelection(),
|
|
218
223
|
bracketMatching(),
|
|
219
224
|
closeBrackets(),
|
|
@@ -233,7 +238,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
233
238
|
// undo steps still available (otherwise it silently performs undo).
|
|
234
239
|
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.key.toLowerCase() === "z") {
|
|
235
240
|
event.preventDefault();
|
|
236
|
-
|
|
241
|
+
redoCommand(view);
|
|
237
242
|
return true;
|
|
238
243
|
}
|
|
239
244
|
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.key.toLowerCase() === "f") {
|
|
@@ -262,7 +267,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
262
267
|
indentWithTab,
|
|
263
268
|
...closeBracketsKeymap,
|
|
264
269
|
...defaultKeymap,
|
|
265
|
-
...
|
|
270
|
+
...historyBindings,
|
|
266
271
|
...searchKeymap,
|
|
267
272
|
...foldKeymap
|
|
268
273
|
]),
|
|
@@ -331,6 +336,13 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
331
336
|
languageName=name;closeOptions();
|
|
332
337
|
view.dispatch({ effects: language.reconfigure(languageExtension(name, completionExtension)) });
|
|
333
338
|
},
|
|
339
|
+
/** Forget the typed edits CodeMirror can undo (the text they were typed against is gone). Returns nothing; the document is unchanged. */
|
|
340
|
+
resetHistory(){
|
|
341
|
+
view.dispatch({ effects: historyField.reconfigure([]) });
|
|
342
|
+
view.dispatch({ effects: historyField.reconfigure(history()) });
|
|
343
|
+
},
|
|
344
|
+
/** How many typed edit groups CodeMirror can still undo. */
|
|
345
|
+
undoDepth: () => undoDepth(view.state),
|
|
334
346
|
setReadOnly(value){view.dispatch({ effects: editable.reconfigure(editableExtension(value)) });},
|
|
335
347
|
/** The CodeMirror view, for hosts that scroll or measure it. */
|
|
336
348
|
view,
|
package/dist/source-pane.d.ts
CHANGED
|
@@ -65,8 +65,14 @@ export interface SourcePaneOptions {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export interface ReplaceDocumentOptions {
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* The text the document was read from: JSON, YAML or Markdown. When it reads as exactly the document, the pane keeps its spelling (JSON
|
|
70
|
+
* formatting, YAML comments, Markdown prose layout), shows it while the pane is in that format and brings it back on Undo, Redo and
|
|
71
|
+
* when the format is switched to it. Text that does not read as the document is ignored.
|
|
72
|
+
*/
|
|
69
73
|
source?: string;
|
|
74
|
+
/** The format of `source`. Default: inferred, the first of JSON, the pane's format, YAML and Markdown that reads as the document. */
|
|
75
|
+
format?: SourceFormat;
|
|
70
76
|
/** The file name without extension, as `setName` (the document's own name is not used). */
|
|
71
77
|
name?: string;
|
|
72
78
|
/** Extra `meta` for the session change (`source` defaults to `"replace-document"`). */
|
|
@@ -78,10 +84,11 @@ export interface SourcePane {
|
|
|
78
84
|
/**
|
|
79
85
|
* Open a different document in the session and the pane (a host's example menu, File > Open): `editor.replaceDocument`, so it is one
|
|
80
86
|
* undoable change and no catalog record is forked into `catalogs.custom` (a diff typed or pasted into the pane is an edit; this is not).
|
|
81
|
-
* `document` is a parsed document or JSON text. The pane shows `options.source` (the
|
|
82
|
-
* byte; `document` itself when it is text) in
|
|
83
|
-
*
|
|
84
|
-
*
|
|
87
|
+
* `document` is a parsed document or JSON text. The pane shows `options.source` (the text the document was read from in JSON, YAML or
|
|
88
|
+
* Markdown, kept byte for byte, comments included; `document` itself when it is JSON text) in the format it is in, and the document
|
|
89
|
+
* written by core's converter when no original text fits. Undo and Redo bring back each document's text exactly, in the format that is
|
|
90
|
+
* shown, and so do Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z once the text typed since the load is undone. A document the session refuses throws
|
|
91
|
+
* (`invalid-opf-edit` with `issues`) and changes nothing. Works in a read-only pane.
|
|
85
92
|
*/
|
|
86
93
|
replaceDocument(document: unknown, options?: ReplaceDocumentOptions): EditorChange;
|
|
87
94
|
/** Re-read the document from the session. */
|
package/dist/source-pane.js
CHANGED
|
@@ -236,6 +236,7 @@ export function createSourcePane(container, options = {}) {
|
|
|
236
236
|
catalogs: options.catalogs ?? editor.catalogs,
|
|
237
237
|
describedBy: `${id}-findings`,
|
|
238
238
|
onChange: () => schedule(),
|
|
239
|
+
historyFallback: { undo: () => stepReplace("undo"), redo: () => stepReplace("redo") },
|
|
239
240
|
onError: (error) => options.onError?.(error),
|
|
240
241
|
});
|
|
241
242
|
|
|
@@ -327,11 +328,7 @@ export function createSourcePane(container, options = {}) {
|
|
|
327
328
|
if (comments > 0 && written.text !== before) transientNotes = [normalizeFinding({ message: `${plural(comments, "comment")} in the YAML ${comments === 1 ? "was" : "were"} dropped when the document changed outside the source.`, severity: "warning" }, "format")];
|
|
328
329
|
}
|
|
329
330
|
// A document `replaceDocument` opened or replaced comes back with the exact text it had.
|
|
330
|
-
|
|
331
|
-
if (exact !== undefined && exact !== written.text) {
|
|
332
|
-
try { if (same(readText(format, exact, catalogs()).deck, deck())) { written = { ...written, text: exact }; if (format === "json") jsonText = exact; } }
|
|
333
|
-
catch { /* The remembered text no longer reads as this document: use the generated one. */ }
|
|
334
|
-
}
|
|
331
|
+
written = preferExact(format, deck(), written);
|
|
335
332
|
noteFindings = written.notes;
|
|
336
333
|
if (format !== "json") syncJsonSafe();
|
|
337
334
|
if (written.text !== control.api.getValue()) show(written.text);
|
|
@@ -339,6 +336,16 @@ export function createSourcePane(container, options = {}) {
|
|
|
339
336
|
renderFindings();
|
|
340
337
|
syncAttributes();
|
|
341
338
|
}
|
|
339
|
+
/** The remembered text of `value` in `target` (what `replaceDocument` was given or replaced) when it still reads as `value`; else `written`. */
|
|
340
|
+
function preferExact(target, value, written) {
|
|
341
|
+
const exact = exactText(target, value);
|
|
342
|
+
if (exact === undefined || exact === written.text) return written;
|
|
343
|
+
try {
|
|
344
|
+
if (!same(readText(target, exact, catalogs()).deck, value)) return written;
|
|
345
|
+
} catch { return written; }
|
|
346
|
+
if (target === "json") jsonText = exact;
|
|
347
|
+
return { ...written, text: exact };
|
|
348
|
+
}
|
|
342
349
|
function syncJsonSafe() { try { syncJson(deck()); } catch { /* Keep the last JSON. */ } }
|
|
343
350
|
|
|
344
351
|
const unsubscribe = editor.subscribe((event) => {
|
|
@@ -346,21 +353,39 @@ export function createSourcePane(container, options = {}) {
|
|
|
346
353
|
if (event.type === "catalogs") { control.setCatalogs(options.catalogs ?? editor.catalogs); renderFindings(); return; }
|
|
347
354
|
if (applying) return;
|
|
348
355
|
version++;
|
|
356
|
+
// Undo, Redo, a restore and a whole-document replace by the host move the text under the typed edits CodeMirror remembers; they
|
|
357
|
+
// would no longer fit it.
|
|
358
|
+
if (event.type === "undo" || event.type === "redo" || event.type === "restore" || event.meta?.replaceDocument) control.resetHistory();
|
|
349
359
|
fromSession();
|
|
350
360
|
});
|
|
351
361
|
|
|
352
362
|
// ---- Replacing the whole document ----------------------------------------------------------------------------------
|
|
363
|
+
/**
|
|
364
|
+
* Which format the text is, among those that read as exactly `opened`: the declared `format` (which must read so), else JSON first
|
|
365
|
+
* (the form `replaceDocument` always took), then the pane's own format, then the others. Null when none reads as the document.
|
|
366
|
+
*/
|
|
367
|
+
function sourceFormatOf(text, opened, declared) {
|
|
368
|
+
const order = declared ? [declared] : [...new Set(["json", format, "yaml", "md"])];
|
|
369
|
+
for (const candidate of order) {
|
|
370
|
+
try {
|
|
371
|
+
const read = readText(candidate, text, catalogs());
|
|
372
|
+
if (read.deck && same(read.deck, opened)) return candidate;
|
|
373
|
+
} catch { /* Not this format. */ }
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
353
377
|
/**
|
|
354
378
|
* Open `input` (a document, or JSON text) as the whole document with `editor.replaceDocument`: one undoable change, no catalog record
|
|
355
|
-
* forked. The text shown is `
|
|
356
|
-
* and
|
|
379
|
+
* forked. The text shown is the original text of the document when `source` (or `input`, when it is JSON text) is that text in the
|
|
380
|
+
* pane's format, and the document written by core's converter otherwise. The original text of any format is kept for the pane's other
|
|
381
|
+
* formats and for Undo and Redo, which bring each document's text back exactly.
|
|
357
382
|
*/
|
|
358
383
|
function replaceDocument(input, replaceOptions = {}) {
|
|
359
384
|
if (destroyed) throw new Error("The source pane was destroyed.");
|
|
385
|
+
if (replaceOptions.format !== undefined && !FORMATS.some((entry) => entry.id === replaceOptions.format)) throw new TypeError(`replaceDocument: format must be "json", "yaml" or "md", not ${JSON.stringify(replaceOptions.format)}.`);
|
|
360
386
|
flush();
|
|
361
387
|
const before = deck();
|
|
362
388
|
const beforeText = dirty ? undefined : control.api.getValue();
|
|
363
|
-
const authored = typeof input === "string" ? input : replaceOptions.source;
|
|
364
389
|
transientNotes = [];
|
|
365
390
|
let change;
|
|
366
391
|
applying = true;
|
|
@@ -380,11 +405,21 @@ export function createSourcePane(container, options = {}) {
|
|
|
380
405
|
rejectionFindings = [];
|
|
381
406
|
dirty = false;
|
|
382
407
|
version++;
|
|
408
|
+
// The typed edits CodeMirror remembers belong to the document that was replaced.
|
|
409
|
+
control.resetHistory();
|
|
383
410
|
const opened = deck();
|
|
384
|
-
|
|
411
|
+
// The original text: JSON text given as `input` is that document's JSON; `source` is its text in `format` (inferred when not given).
|
|
385
412
|
let authoredJson = null;
|
|
386
|
-
|
|
413
|
+
const originals = [];
|
|
414
|
+
if (typeof input === "string") originals.push({ text: input, format: "json" });
|
|
415
|
+
if (typeof replaceOptions.source === "string" && (typeof input !== "string" || replaceOptions.format)) originals.push({ text: replaceOptions.source, format: replaceOptions.format });
|
|
416
|
+
for (const original of originals) {
|
|
417
|
+
const kind = sourceFormatOf(original.text, opened, original.format);
|
|
418
|
+
if (kind === "json") authoredJson ??= original.text;
|
|
419
|
+
else if (kind) rememberText(kind, opened, original.text);
|
|
420
|
+
}
|
|
387
421
|
if (replaceOptions.name !== undefined) { name = sanitizeName(replaceOptions.name) || "deck"; updateBar(); }
|
|
422
|
+
let written;
|
|
388
423
|
try { written = format === "json" && authoredJson !== null ? { text: authoredJson, notes: [] } : write(format, opened); }
|
|
389
424
|
catch (error) {
|
|
390
425
|
syncJsonSafe();
|
|
@@ -393,6 +428,8 @@ export function createSourcePane(container, options = {}) {
|
|
|
393
428
|
return change;
|
|
394
429
|
}
|
|
395
430
|
if (authoredJson !== null) jsonText = authoredJson; else syncJsonSafe();
|
|
431
|
+
// The text shown now is what Undo and Redo bring back for this document, in the format shown.
|
|
432
|
+
if (!(format === "json" && authoredJson !== null)) written = preferExact(format, opened, written);
|
|
396
433
|
rememberText(format, opened, written.text);
|
|
397
434
|
rememberText(format, before, beforeText);
|
|
398
435
|
noteFindings = written.notes;
|
|
@@ -403,6 +440,29 @@ export function createSourcePane(container, options = {}) {
|
|
|
403
440
|
return change;
|
|
404
441
|
}
|
|
405
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z (Ctrl+Y) after CodeMirror's own history has nothing left: step the session over a whole-document
|
|
445
|
+
* replace, as its Undo and Redo buttons do, so the text comes back exactly. Undo steps back over the nearest `replaceDocument` together
|
|
446
|
+
* with the pane's own edits typed on it (CodeMirror already undid the typing); an edit made elsewhere (the canvas, a panel) stops it.
|
|
447
|
+
* Redo steps forward over a replace only. Returns whether it did something.
|
|
448
|
+
*/
|
|
449
|
+
function stepReplace(direction) {
|
|
450
|
+
if (destroyed || readOnly || typeof editor.exportHistory !== "function") return false;
|
|
451
|
+
flush();
|
|
452
|
+
if (dirty || !(direction === "undo" ? editor.canUndo : editor.canRedo)) return false;
|
|
453
|
+
const history = editor.exportHistory();
|
|
454
|
+
if (direction === "redo") {
|
|
455
|
+
if (!history.redo.at(-1)?.meta?.replaceDocument) return false;
|
|
456
|
+
editor.redo({ source: "source-pane" });
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
let index = history.undo.length - 1;
|
|
460
|
+
while (index >= 0 && !history.undo[index].meta?.replaceDocument && history.undo[index].meta?.source === "source-pane") index -= 1;
|
|
461
|
+
if (index < 0 || !history.undo[index].meta?.replaceDocument) return false;
|
|
462
|
+
for (let steps = history.undo.length - index; steps > 0; steps -= 1) editor.undo({ source: "source-pane" });
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
406
466
|
// ---- Format -------------------------------------------------------------------------------------------------------
|
|
407
467
|
function setFormatInternal(next, notify) {
|
|
408
468
|
const target = formatOf(next);
|
|
@@ -413,7 +473,7 @@ export function createSourcePane(container, options = {}) {
|
|
|
413
473
|
const draft = drafts[target.id];
|
|
414
474
|
if (draft && draft.version === version) written = { text: draft.text, notes: [], draft: true };
|
|
415
475
|
else {
|
|
416
|
-
try { written = write(target.id, deck()); }
|
|
476
|
+
try { written = preferExact(target.id, deck(), write(target.id, deck())); }
|
|
417
477
|
catch (error) {
|
|
418
478
|
rejectionFindings = [normalizeFinding({ message: `The document cannot be written as ${target.label}: ${errorText(error)} Fix the findings first.`, kind: "rejected" })];
|
|
419
479
|
renderFindings();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0-rc.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/OpenPresentation/opf
|
|
13
|
+
"url": "git+https://github.com/OpenPresentation/opf.git",
|
|
14
|
+
"directory": "packages/editor"
|
|
14
15
|
},
|
|
15
16
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/OpenPresentation/opf
|
|
17
|
+
"url": "https://github.com/OpenPresentation/opf/issues"
|
|
17
18
|
},
|
|
18
|
-
"homepage": "https://github.com/OpenPresentation/opf
|
|
19
|
+
"homepage": "https://github.com/OpenPresentation/opf/tree/main/packages/editor#readme",
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"access": "public"
|
|
21
22
|
},
|
|
@@ -269,52 +270,6 @@
|
|
|
269
270
|
},
|
|
270
271
|
"types": "./dist/index.d.ts",
|
|
271
272
|
"sideEffects": false,
|
|
272
|
-
"scripts": {
|
|
273
|
-
"build": "node scripts/build.mjs",
|
|
274
|
-
"typecheck": "node scripts/check-syntax.mjs src test scripts examples",
|
|
275
|
-
"test": "npm run build && npm run check:font-hotlinks && node scripts/run-tests.mjs && npm run check:runners && npm run check:changes",
|
|
276
|
-
"test:contract": "npm run build && node scripts/run-tests.mjs --suite contract",
|
|
277
|
-
"check:runners": "node --test scripts/run-tests.test.mjs",
|
|
278
|
-
"check:changes": "node --test scripts/changelog-fragments.test.mjs && node scripts/changelog-fragments.mjs check",
|
|
279
|
-
"validate": "node scripts/validate-package.mjs",
|
|
280
|
-
"test:packed": "node test/packed-install.mjs",
|
|
281
|
-
"prepack": "npm run build",
|
|
282
|
-
"check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
|
|
283
|
-
"check:font-hotlinks:built": "node scripts/check-font-hotlinks.mjs --built dist artifacts/playground",
|
|
284
|
-
"build:playground": "node scripts/build-playground.mjs",
|
|
285
|
-
"test:playground": "npm run build:playground && npm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-base-fonts.mjs artifacts/playground-base-fonts && node test/playground-source.mjs && node test/design-controls-browser.mjs && node test/design-gaps-browser.mjs && node test/chart-options-browser.mjs && node test/persistence-browser.mjs && node test/slides-browser.mjs && node test/slide-navigator-browser.mjs && node test/layout-templates-browser.mjs && node test/layout-editor-browser.mjs && node test/template-panel-browser.mjs && node test/numbering-panel-browser.mjs && node test/playground-ensure-fonts.mjs artifacts/playground-ensure-fonts && node test/canvas-fonts-browser.mjs artifacts/canvas-fonts && node test/find-replace-browser.mjs && node test/image-crop-browser.mjs && node test/mobile-browser.mjs && node test/review-panel-browser.mjs && node test/review-hook-browser.mjs && node test/source-pane-browser.mjs && node test/playground-download.mjs artifacts/playground-download && node test/export-menu-browser.mjs && node test/agent-handoff-browser.mjs && node test/presenter-browser.mjs && node test/present-button-browser.mjs",
|
|
286
|
-
"test:code-browser": "node test/code-browser.mjs",
|
|
287
|
-
"test:selection-browser": "node test/selection-browser.mjs",
|
|
288
|
-
"test:click-entry-browser": "node test/click-entry-browser.mjs",
|
|
289
|
-
"test:canvas-a11y-browser": "node test/canvas-a11y-browser.mjs",
|
|
290
|
-
"test:metric-browser": "node test/metric-browser.mjs",
|
|
291
|
-
"test:rich-input-browser": "node test/rich-input-browser.mjs",
|
|
292
|
-
"test:json": "npm run build && node test/json-source.mjs && node test/json-options.mjs",
|
|
293
|
-
"test:json-browser": "node test/json-editor-browser.mjs",
|
|
294
|
-
"test:design-controls-browser": "node test/design-controls-browser.mjs",
|
|
295
|
-
"test:template-panel-browser": "node test/template-panel-browser.mjs",
|
|
296
|
-
"test:design-gaps-browser": "node test/design-gaps-browser.mjs",
|
|
297
|
-
"test:data-grid-browser": "node test/data-grid-browser.mjs",
|
|
298
|
-
"test:data-grid-datasets-browser": "node test/data-grid-datasets-browser.mjs",
|
|
299
|
-
"test:data-import-dataset-browser": "node test/data-import-dataset-browser.mjs",
|
|
300
|
-
"test:slides-browser": "node test/slides-browser.mjs",
|
|
301
|
-
"test:slide-navigator-browser": "node test/slide-navigator-browser.mjs",
|
|
302
|
-
"test:persistence-browser": "node test/persistence-browser.mjs",
|
|
303
|
-
"test:find-browser": "node test/find-replace-browser.mjs",
|
|
304
|
-
"test:crop-browser": "node test/image-crop-browser.mjs",
|
|
305
|
-
"test:mobile-browser": "node test/mobile-browser.mjs",
|
|
306
|
-
"test:review-panel-browser": "node test/review-panel-browser.mjs",
|
|
307
|
-
"test:review-hook-browser": "node test/review-hook-browser.mjs",
|
|
308
|
-
"test:source-pane-browser": "node test/source-pane-browser.mjs",
|
|
309
|
-
"test:layout-editor-browser": "node test/layout-editor-browser.mjs",
|
|
310
|
-
"test:export-license-browser": "node test/export-license-browser.mjs",
|
|
311
|
-
"test:chart-options-browser": "node test/chart-options-browser.mjs",
|
|
312
|
-
"test:numbering-panel-browser": "node test/numbering-panel-browser.mjs",
|
|
313
|
-
"test:presenter-browser": "node test/presenter-browser.mjs",
|
|
314
|
-
"test:present-button-browser": "node test/present-button-browser.mjs",
|
|
315
|
-
"test:export-menu-browser": "node test/export-menu-browser.mjs",
|
|
316
|
-
"test:agent-handoff-browser": "node test/agent-handoff-browser.mjs"
|
|
317
|
-
},
|
|
318
273
|
"dependencies": {
|
|
319
274
|
"@codemirror/autocomplete": "6.20.3",
|
|
320
275
|
"@codemirror/commands": "6.11.1",
|
|
@@ -326,11 +281,11 @@
|
|
|
326
281
|
"@codemirror/state": "6.7.6",
|
|
327
282
|
"@codemirror/view": "6.43.14",
|
|
328
283
|
"@lezer/highlight": "1.2.5",
|
|
329
|
-
"
|
|
330
|
-
"
|
|
284
|
+
"jsonc-parser": "3.3.1",
|
|
285
|
+
"@openpresentation/opf": "^0.26.0-rc.0"
|
|
331
286
|
},
|
|
332
287
|
"peerDependencies": {
|
|
333
|
-
"@openpresentation/opf-render": "^0.
|
|
288
|
+
"@openpresentation/opf-render": "^0.26.0-rc.0"
|
|
334
289
|
},
|
|
335
290
|
"peerDependenciesMeta": {
|
|
336
291
|
"@openpresentation/opf-render": {
|
|
@@ -338,7 +293,6 @@
|
|
|
338
293
|
}
|
|
339
294
|
},
|
|
340
295
|
"devDependencies": {
|
|
341
|
-
"@openpresentation/gallery": "^5.0.0",
|
|
342
296
|
"@expo-google-fonts/arimo": "0.4.3",
|
|
343
297
|
"@expo-google-fonts/caladea": "0.4.2",
|
|
344
298
|
"@expo-google-fonts/cousine": "0.4.3",
|
|
@@ -383,14 +337,60 @@
|
|
|
383
337
|
"@expo-google-fonts/roboto-mono": "0.4.2",
|
|
384
338
|
"@expo-google-fonts/stix-two-math": "0.4.0",
|
|
385
339
|
"@expo-google-fonts/tinos": "0.4.2",
|
|
386
|
-
"@openpresentation/opf-pptx": "^0.25.0",
|
|
387
|
-
"@openpresentation/opf-render": "^0.25.0",
|
|
388
340
|
"@resvg/resvg-js": "2.6.2",
|
|
389
341
|
"axe-core": "4.14.0",
|
|
390
342
|
"esbuild": "0.28.2",
|
|
391
343
|
"jszip": "3.10.2",
|
|
392
344
|
"pdf-lib": "1.17.1",
|
|
393
345
|
"pdfjs-dist": "6.4.299",
|
|
394
|
-
"playwright": "1.64.0"
|
|
395
|
-
|
|
346
|
+
"playwright": "1.64.0",
|
|
347
|
+
"@openpresentation/gallery": "0.26.0-rc.0",
|
|
348
|
+
"@openpresentation/opf-pptx": "0.26.0-rc.0",
|
|
349
|
+
"@openpresentation/opf-render": "0.26.0-rc.0"
|
|
350
|
+
},
|
|
351
|
+
"scripts": {
|
|
352
|
+
"build": "node scripts/build.mjs",
|
|
353
|
+
"build:deps": "pnpm --filter \"@openpresentation/opf-editor^...\" --fail-if-no-match run build",
|
|
354
|
+
"typecheck": "node scripts/check-syntax.mjs src test scripts examples",
|
|
355
|
+
"test": "pnpm run build:deps && pnpm run build && node ../../scripts/run-tests.mjs && pnpm run check:scripts",
|
|
356
|
+
"test:contract": "pnpm run build:deps && pnpm run build && node ../../scripts/run-tests.mjs --suite contract",
|
|
357
|
+
"validate": "node scripts/validate-package.mjs",
|
|
358
|
+
"check:scripts": "node --test scripts/check-syntax.test.mjs scripts/browser-suites.test.mjs",
|
|
359
|
+
"test:packed": "pnpm run build:deps && node test/packed-install.mjs",
|
|
360
|
+
"check:font-hotlinks:built": "node ../../scripts/check-font-hotlinks.mjs --built dist artifacts/playground",
|
|
361
|
+
"build:playground": "node scripts/build-playground.mjs",
|
|
362
|
+
"test:playground": "pnpm run build:deps && pnpm run build && pnpm run build:playground && pnpm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-base-fonts.mjs artifacts/playground-base-fonts && node test/playground-source.mjs && node test/design-controls-browser.mjs && node test/design-gaps-browser.mjs && node test/chart-options-browser.mjs && node test/persistence-browser.mjs && node test/slides-browser.mjs && node test/slide-navigator-browser.mjs && node test/layout-templates-browser.mjs && node test/layout-editor-browser.mjs && node test/template-panel-browser.mjs && node test/numbering-panel-browser.mjs && node test/playground-ensure-fonts.mjs artifacts/playground-ensure-fonts && node test/canvas-fonts-browser.mjs artifacts/canvas-fonts && node test/find-replace-browser.mjs && node test/image-crop-browser.mjs && node test/mobile-browser.mjs && node test/review-panel-browser.mjs && node test/review-hook-browser.mjs && node test/source-pane-browser.mjs && node test/playground-download.mjs artifacts/playground-download && node test/export-menu-browser.mjs && node test/agent-handoff-browser.mjs && node test/presenter-browser.mjs && node test/present-button-browser.mjs",
|
|
363
|
+
"test:code-browser": "node test/code-browser.mjs",
|
|
364
|
+
"test:selection-browser": "node test/selection-browser.mjs",
|
|
365
|
+
"test:click-entry-browser": "node test/click-entry-browser.mjs",
|
|
366
|
+
"test:canvas-a11y-browser": "node test/canvas-a11y-browser.mjs",
|
|
367
|
+
"test:metric-browser": "node test/metric-browser.mjs",
|
|
368
|
+
"test:rich-input-browser": "node test/rich-input-browser.mjs",
|
|
369
|
+
"test:json": "pnpm run build:deps && pnpm run build && node test/json-source.mjs && node test/json-options.mjs",
|
|
370
|
+
"test:json-browser": "node test/json-editor-browser.mjs",
|
|
371
|
+
"test:design-controls-browser": "node test/design-controls-browser.mjs",
|
|
372
|
+
"test:template-panel-browser": "node test/template-panel-browser.mjs",
|
|
373
|
+
"test:design-gaps-browser": "node test/design-gaps-browser.mjs",
|
|
374
|
+
"test:data-grid-browser": "node test/data-grid-browser.mjs",
|
|
375
|
+
"test:data-grid-datasets-browser": "node test/data-grid-datasets-browser.mjs",
|
|
376
|
+
"test:data-import-dataset-browser": "node test/data-import-dataset-browser.mjs",
|
|
377
|
+
"test:slides-browser": "node test/slides-browser.mjs",
|
|
378
|
+
"test:slide-navigator-browser": "node test/slide-navigator-browser.mjs",
|
|
379
|
+
"test:persistence-browser": "node test/persistence-browser.mjs",
|
|
380
|
+
"test:find-browser": "node test/find-replace-browser.mjs",
|
|
381
|
+
"test:crop-browser": "node test/image-crop-browser.mjs",
|
|
382
|
+
"test:mobile-browser": "node test/mobile-browser.mjs",
|
|
383
|
+
"test:review-panel-browser": "node test/review-panel-browser.mjs",
|
|
384
|
+
"test:review-hook-browser": "node test/review-hook-browser.mjs",
|
|
385
|
+
"test:source-pane-browser": "node test/source-pane-browser.mjs",
|
|
386
|
+
"test:layout-editor-browser": "node test/layout-editor-browser.mjs",
|
|
387
|
+
"test:export-license-browser": "node test/export-license-browser.mjs",
|
|
388
|
+
"test:chart-options-browser": "node test/chart-options-browser.mjs",
|
|
389
|
+
"test:numbering-panel-browser": "node test/numbering-panel-browser.mjs",
|
|
390
|
+
"test:presenter-browser": "node test/presenter-browser.mjs",
|
|
391
|
+
"test:present-button-browser": "node test/present-button-browser.mjs",
|
|
392
|
+
"test:export-menu-browser": "node test/export-menu-browser.mjs",
|
|
393
|
+
"test:agent-handoff-browser": "node test/agent-handoff-browser.mjs"
|
|
394
|
+
},
|
|
395
|
+
"gitHead": "fb80b1e8a7d6dd4a77946509fab9af7713657ce6"
|
|
396
396
|
}
|