@openpresentation/opf-editor 0.0.1
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/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +709 -0
- package/dist/react.d.ts +31 -0
- package/dist/react.js +94 -0
- package/dist/svelte.d.ts +29 -0
- package/dist/svelte.js +112 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenPresentation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# OPF Editor
|
|
2
|
+
|
|
3
|
+
Embeddable local editor primitives for Open Presentation Format documents. The package turns traced `@openpresentation/opf-render` SVG output into JSON-path-aware edits, validates OPF after each change, and records undo/redo as JSON Patch operations.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
- Package: `@openpresentation/opf-editor`
|
|
8
|
+
- Repository: `OpenPresentation/opf-editor`
|
|
9
|
+
- License: MIT
|
|
10
|
+
- Compatibility target: `@openpresentation/opf`
|
|
11
|
+
- Renderer relationship: built on `@openpresentation/opf-render` trace output
|
|
12
|
+
- Headless JSON edit bindings for renderer trace attributes
|
|
13
|
+
- Structured catalog controls that only commit known catalog IDs
|
|
14
|
+
- JSON Patch state transitions with inverse patches for undo/redo
|
|
15
|
+
- Optional DOM controls plus React and Svelte bindings in separate embeddable entry points
|
|
16
|
+
|
|
17
|
+
## Runtime Policy
|
|
18
|
+
|
|
19
|
+
The package runtime must stay local and deterministic:
|
|
20
|
+
|
|
21
|
+
- No hosted service in the critical path
|
|
22
|
+
- No telemetry or hidden analytics
|
|
23
|
+
- No commercial SDK dependency in the critical path
|
|
24
|
+
- No required network calls
|
|
25
|
+
- Host applications own auth, storage, queues, analytics, collaboration, branding, and product workflow
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm ci
|
|
31
|
+
npm run build
|
|
32
|
+
npm run typecheck
|
|
33
|
+
npm test
|
|
34
|
+
npm run validate
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Headless Editing
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import {
|
|
41
|
+
createEditorSession,
|
|
42
|
+
createSvgTraceBinding
|
|
43
|
+
} from "@openpresentation/opf-editor";
|
|
44
|
+
import { renderSvg } from "@openpresentation/opf-render";
|
|
45
|
+
|
|
46
|
+
const editor = createEditorSession(opfDocument, { rejectInvalid: true });
|
|
47
|
+
const svg = renderSvg(editor.document, { trace: true });
|
|
48
|
+
|
|
49
|
+
preview.innerHTML = svg;
|
|
50
|
+
const binding = createSvgTraceBinding(preview.querySelector("svg"), editor, {
|
|
51
|
+
onSelect(selection) {
|
|
52
|
+
inspector.open({
|
|
53
|
+
path: selection.path,
|
|
54
|
+
value: selection.value,
|
|
55
|
+
commit(value) {
|
|
56
|
+
binding.commit(selection.element, value);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every change returns JSON Patch operations plus inverse operations:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const change = editor.set("slides.0.title", "Quarterly plan");
|
|
67
|
+
|
|
68
|
+
change.patches;
|
|
69
|
+
// [{ op: "replace", path: "/slides/0/title", value: "Quarterly plan" }]
|
|
70
|
+
|
|
71
|
+
change.inversePatches;
|
|
72
|
+
// [{ op: "replace", path: "/slides/0/title", value: "Old title" }]
|
|
73
|
+
|
|
74
|
+
editor.undo();
|
|
75
|
+
editor.redo();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`createSvgTraceBinding` only reads `data-opf-path`; it does not require a hosted service, a second renderer, storage, analytics, auth, or product chrome. Host applications decide how selected paths appear in their own inspector UI.
|
|
79
|
+
|
|
80
|
+
## Catalog Controls
|
|
81
|
+
|
|
82
|
+
Catalog helpers commit catalog IDs from the OPF catalog registry instead of accepting freeform incompatible values:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import {
|
|
86
|
+
createCatalogSelect,
|
|
87
|
+
getCatalogOptions
|
|
88
|
+
} from "@openpresentation/opf-editor";
|
|
89
|
+
|
|
90
|
+
getCatalogOptions("themes").map((option) => option.id);
|
|
91
|
+
|
|
92
|
+
const themeSelect = createCatalogSelect(editor, {
|
|
93
|
+
path: "design.theme",
|
|
94
|
+
catalogKind: "themes",
|
|
95
|
+
label: "Theme"
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
toolbar.append(themeSelect);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Invalid IDs throw before they reach the document. Existing object-form references keep their sibling override fields and replace only `id`.
|
|
102
|
+
|
|
103
|
+
## Optional React Bindings
|
|
104
|
+
|
|
105
|
+
React bindings are isolated under `@openpresentation/opf-editor/react` and require the host app to pass its React runtime. The core package does not add React to the critical path.
|
|
106
|
+
|
|
107
|
+
```jsx
|
|
108
|
+
import { createOPFReactComponents } from "@openpresentation/opf-editor/react";
|
|
109
|
+
|
|
110
|
+
const { OPFTextInput, OPFCatalogSelect } = createOPFReactComponents(React);
|
|
111
|
+
|
|
112
|
+
function Inspector({ editor }) {
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<OPFTextInput editor={editor} path="slides.0.title" label="Slide title" />
|
|
116
|
+
<OPFCatalogSelect editor={editor} path="design.theme" catalogKind="themes" label="Theme" />
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Optional Svelte Bindings
|
|
123
|
+
|
|
124
|
+
Svelte bindings are isolated under `@openpresentation/opf-editor/svelte` as actions. They bind host-owned controls, so the package does not ship product chrome or require the Svelte runtime in the core entry point.
|
|
125
|
+
|
|
126
|
+
```svelte
|
|
127
|
+
<script>
|
|
128
|
+
import { opfCatalogSelect, opfTextInput } from "@openpresentation/opf-editor/svelte";
|
|
129
|
+
|
|
130
|
+
export let editor;
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<input use:opfTextInput={{ editor, path: "slides.0.title", label: "Slide title" }} />
|
|
134
|
+
<select use:opfCatalogSelect={{ editor, path: "design.theme", catalogKind: "themes", label: "Theme" }} />
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Release Lane
|
|
138
|
+
|
|
139
|
+
Public npm package publication is handled by `.github/workflows/release.yml` with npm provenance.
|
|
140
|
+
|
|
141
|
+
Required first-publish setup:
|
|
142
|
+
|
|
143
|
+
1. An npm owner for the `@openpresentation` scope must run the first publish or reserve/grant the `@openpresentation/opf-editor` package.
|
|
144
|
+
2. Configure npm Trusted Publishing for GitHub repository `OpenPresentation/opf-editor` and workflow `.github/workflows/release.yml`.
|
|
145
|
+
3. Publish by creating a GitHub Release or manually running the Release workflow after CI passes.
|
|
146
|
+
|
|
147
|
+
This repo does not require an npm automation token when Trusted Publishing is configured.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export declare const packageName = "@openpresentation/opf-editor";
|
|
2
|
+
|
|
3
|
+
export declare const releaseLane: Readonly<{
|
|
4
|
+
githubRepository: "OpenPresentation/opf-editor";
|
|
5
|
+
npmPackage: "@openpresentation/opf-editor";
|
|
6
|
+
compatibilityPackage: "@openpresentation/opf";
|
|
7
|
+
rendererPackage: "@openpresentation/opf-render";
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export declare const runtimePolicy: Readonly<{
|
|
11
|
+
hostedServiceInCriticalPath: false;
|
|
12
|
+
telemetry: false;
|
|
13
|
+
commercialSdkInCriticalPath: false;
|
|
14
|
+
requiredNetworkCalls: false;
|
|
15
|
+
deterministicLocalExecution: true;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type JsonPatchOperation =
|
|
19
|
+
| { op: "add"; path: string; value: unknown }
|
|
20
|
+
| { op: "replace"; path: string; value: unknown }
|
|
21
|
+
| { op: "remove"; path: string };
|
|
22
|
+
|
|
23
|
+
export interface OPFValidationSummary {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
errors: unknown[];
|
|
26
|
+
result: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EditorSnapshot {
|
|
30
|
+
document: unknown;
|
|
31
|
+
validation: OPFValidationSummary;
|
|
32
|
+
canUndo: boolean;
|
|
33
|
+
canRedo: boolean;
|
|
34
|
+
undoDepth: number;
|
|
35
|
+
redoDepth: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface EditorChange {
|
|
39
|
+
document: unknown;
|
|
40
|
+
patches: JsonPatchOperation[];
|
|
41
|
+
inversePatches?: JsonPatchOperation[];
|
|
42
|
+
redoPatches?: JsonPatchOperation[];
|
|
43
|
+
validation: OPFValidationSummary;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EditorEvent {
|
|
47
|
+
type: "patch" | "undo" | "redo";
|
|
48
|
+
patches: JsonPatchOperation[];
|
|
49
|
+
inversePatches?: JsonPatchOperation[];
|
|
50
|
+
redoPatches?: JsonPatchOperation[];
|
|
51
|
+
validation: OPFValidationSummary;
|
|
52
|
+
meta?: Record<string, unknown>;
|
|
53
|
+
snapshot: EditorSnapshot;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface EditorSession {
|
|
57
|
+
readonly document: unknown;
|
|
58
|
+
readonly validation: OPFValidationSummary;
|
|
59
|
+
readonly canUndo: boolean;
|
|
60
|
+
readonly canRedo: boolean;
|
|
61
|
+
snapshot(): EditorSnapshot;
|
|
62
|
+
subscribe(listener: (event: EditorEvent) => void): () => void;
|
|
63
|
+
get(path: string | string[], fallback?: unknown): unknown;
|
|
64
|
+
set(path: string | string[], value: unknown, meta?: Record<string, unknown>): EditorChange;
|
|
65
|
+
setCatalog(path: string | string[], catalogKind: string, id: string, meta?: Record<string, unknown>): EditorChange;
|
|
66
|
+
applyPatch(operations: JsonPatchOperation[], meta?: Record<string, unknown>): EditorChange;
|
|
67
|
+
undo(meta?: Record<string, unknown>): EditorChange | null;
|
|
68
|
+
redo(meta?: Record<string, unknown>): EditorChange | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CreateEditorSessionOptions {
|
|
72
|
+
validate?: (document: unknown) => unknown;
|
|
73
|
+
rejectInvalid?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CatalogOption {
|
|
77
|
+
id: string;
|
|
78
|
+
label: string;
|
|
79
|
+
record: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CatalogOptionsInput {
|
|
83
|
+
presentation?: { catalogs?: Record<string, unknown> } | Record<string, unknown>;
|
|
84
|
+
catalogs?: Record<string, unknown>;
|
|
85
|
+
catalogSources?: Record<string, unknown>;
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SvgTraceBinding {
|
|
90
|
+
elements: unknown[];
|
|
91
|
+
getPath(element: unknown): string;
|
|
92
|
+
getValue(elementOrPath: unknown, fallback?: unknown): unknown;
|
|
93
|
+
select(element: unknown, sourceEvent?: unknown): SvgTraceSelection;
|
|
94
|
+
commit(elementOrPath: unknown, value: unknown, meta?: Record<string, unknown>): EditorChange;
|
|
95
|
+
destroy(): void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SvgTraceSelection {
|
|
99
|
+
path: string;
|
|
100
|
+
pointer: string;
|
|
101
|
+
value: unknown;
|
|
102
|
+
element: unknown;
|
|
103
|
+
editor: EditorSession;
|
|
104
|
+
sourceEvent?: unknown;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SvgTraceBindingOptions {
|
|
108
|
+
selector?: string;
|
|
109
|
+
interactive?: boolean;
|
|
110
|
+
onSelect?: (selection: SvgTraceSelection) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface TextInputOptions {
|
|
114
|
+
path: string;
|
|
115
|
+
label?: string;
|
|
116
|
+
multiline?: boolean;
|
|
117
|
+
type?: string;
|
|
118
|
+
document?: { createElement(tagName: string): any };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CatalogSelectOptions extends CatalogOptionsInput {
|
|
122
|
+
path: string;
|
|
123
|
+
catalogKind: string;
|
|
124
|
+
label?: string;
|
|
125
|
+
document?: { createElement(tagName: string): any };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export declare class OPFEditorError extends Error {
|
|
129
|
+
readonly code: string;
|
|
130
|
+
readonly details: Record<string, unknown>;
|
|
131
|
+
readonly path?: unknown;
|
|
132
|
+
readonly issues?: unknown[];
|
|
133
|
+
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export declare function splitOpfPath(path: string | string[]): string[];
|
|
137
|
+
|
|
138
|
+
export declare function opfPathToJsonPointer(path: string | string[]): string;
|
|
139
|
+
|
|
140
|
+
export declare function jsonPointerToOpfPath(pointer: string): string;
|
|
141
|
+
|
|
142
|
+
export declare function getValueAtPath(document: unknown, path: string | string[], fallback?: unknown): unknown;
|
|
143
|
+
|
|
144
|
+
export declare function hasValueAtPath(document: unknown, path: string | string[]): boolean;
|
|
145
|
+
|
|
146
|
+
export declare function createValuePatch(document: unknown, path: string | string[], value: unknown): JsonPatchOperation[];
|
|
147
|
+
|
|
148
|
+
export declare function applyJsonPatch(document: unknown, operations: JsonPatchOperation[]): unknown;
|
|
149
|
+
|
|
150
|
+
export declare function invertJsonPatch(document: unknown, operations: JsonPatchOperation[]): JsonPatchOperation[];
|
|
151
|
+
|
|
152
|
+
export declare function validateOpfDocument(document: unknown, validator?: (document: unknown) => unknown): OPFValidationSummary;
|
|
153
|
+
|
|
154
|
+
export declare function createEditorSession(input: unknown, options?: CreateEditorSessionOptions): EditorSession;
|
|
155
|
+
|
|
156
|
+
export declare function createSvgTraceBinding(
|
|
157
|
+
root: { querySelectorAll(selector: string): Iterable<unknown>; matches?: (selector: string) => boolean },
|
|
158
|
+
editor: EditorSession,
|
|
159
|
+
options?: SvgTraceBindingOptions
|
|
160
|
+
): SvgTraceBinding;
|
|
161
|
+
|
|
162
|
+
export declare function getCatalogRecords(catalogKind: string, options?: CatalogOptionsInput): Record<string, unknown>[];
|
|
163
|
+
|
|
164
|
+
export declare function getCatalogOptions(catalogKind: string, options?: CatalogOptionsInput): CatalogOption[];
|
|
165
|
+
|
|
166
|
+
export declare function assertCatalogId(catalogKind: string, id: string, options?: CatalogOptionsInput): string;
|
|
167
|
+
|
|
168
|
+
export declare function setCatalogId(
|
|
169
|
+
editor: EditorSession,
|
|
170
|
+
path: string | string[],
|
|
171
|
+
catalogKind: string,
|
|
172
|
+
id: string,
|
|
173
|
+
meta?: CatalogOptionsInput
|
|
174
|
+
): EditorChange;
|
|
175
|
+
|
|
176
|
+
export declare function createTextInput(editor: EditorSession, options: TextInputOptions): any;
|
|
177
|
+
|
|
178
|
+
export declare function createCatalogSelect(editor: EditorSession, options: CatalogSelectOptions): any;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import {
|
|
2
|
+
catalogKinds,
|
|
3
|
+
catalogs as bundledCatalogs,
|
|
4
|
+
validatePresentation
|
|
5
|
+
} from "@openpresentation/opf";
|
|
6
|
+
|
|
7
|
+
export const packageName = "@openpresentation/opf-editor";
|
|
8
|
+
|
|
9
|
+
export const releaseLane = Object.freeze({
|
|
10
|
+
githubRepository: "OpenPresentation/opf-editor",
|
|
11
|
+
npmPackage: "@openpresentation/opf-editor",
|
|
12
|
+
compatibilityPackage: "@openpresentation/opf",
|
|
13
|
+
rendererPackage: "@openpresentation/opf-render"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const runtimePolicy = Object.freeze({
|
|
17
|
+
hostedServiceInCriticalPath: false,
|
|
18
|
+
telemetry: false,
|
|
19
|
+
commercialSdkInCriticalPath: false,
|
|
20
|
+
requiredNetworkCalls: false,
|
|
21
|
+
deterministicLocalExecution: true
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const DATA_OPF_PATH = "data-opf-path";
|
|
25
|
+
const COMPONENT_ATTR = "data-opf-component";
|
|
26
|
+
const SELECT_EVENT = "opfselect";
|
|
27
|
+
const MISSING = Symbol("opf-editor.missing");
|
|
28
|
+
|
|
29
|
+
export class OPFEditorError extends Error {
|
|
30
|
+
constructor(code, message, details = {}) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "OPFEditorError";
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.details = details;
|
|
35
|
+
if (details.path) this.path = details.path;
|
|
36
|
+
if (details.issues) this.issues = details.issues;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function splitOpfPath(path) {
|
|
41
|
+
if (Array.isArray(path)) return path.map((segment) => String(segment));
|
|
42
|
+
if (typeof path !== "string") {
|
|
43
|
+
throw new OPFEditorError("invalid-path", "OPF path must be a string or segment array.", { path });
|
|
44
|
+
}
|
|
45
|
+
if (path === "") return [];
|
|
46
|
+
if (path.startsWith("/")) return parseJsonPointer(path);
|
|
47
|
+
return path.split(".").filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function opfPathToJsonPointer(path) {
|
|
51
|
+
const segments = splitOpfPath(path);
|
|
52
|
+
if (!segments.length) return "";
|
|
53
|
+
return `/${segments.map(escapePointerSegment).join("/")}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function jsonPointerToOpfPath(pointer) {
|
|
57
|
+
return parseJsonPointer(pointer).join(".");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getValueAtPath(document, path, fallback) {
|
|
61
|
+
const value = readAtPath(document, splitOpfPath(path));
|
|
62
|
+
return value === MISSING ? fallback : value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hasValueAtPath(document, path) {
|
|
66
|
+
return readAtPath(document, splitOpfPath(path)) !== MISSING;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createValuePatch(document, path, value) {
|
|
70
|
+
const pointer = normalizePatchPath(path);
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
op: hasValueAtPath(document, pointer) ? "replace" : "add",
|
|
74
|
+
path: pointer,
|
|
75
|
+
value: clone(value)
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function applyJsonPatch(document, operations) {
|
|
81
|
+
assertPatchOperations(operations);
|
|
82
|
+
let next = clone(document);
|
|
83
|
+
for (const operation of operations) {
|
|
84
|
+
next = applyJsonPatchOperation(next, normalizeOperation(operation));
|
|
85
|
+
}
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function invertJsonPatch(document, operations) {
|
|
90
|
+
assertPatchOperations(operations);
|
|
91
|
+
let current = clone(document);
|
|
92
|
+
const inverse = [];
|
|
93
|
+
|
|
94
|
+
for (const operation of operations.map(normalizeOperation)) {
|
|
95
|
+
const previous = readAtPath(current, parseJsonPointer(operation.path));
|
|
96
|
+
if (operation.op !== "add" && previous === MISSING) {
|
|
97
|
+
throw new OPFEditorError("patch-path-missing", `Cannot invert ${operation.op} for missing path ${operation.path}.`, {
|
|
98
|
+
path: operation.path,
|
|
99
|
+
operation
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const addReplacedObjectValue = operation.op === "add" && previous !== MISSING && !pathParentIsArray(current, operation.path);
|
|
103
|
+
const inversePath = operation.op === "add" ? insertedPathForAdd(operation.path, previous, current) : operation.path;
|
|
104
|
+
|
|
105
|
+
current = applyJsonPatchOperation(current, operation);
|
|
106
|
+
|
|
107
|
+
if (addReplacedObjectValue) {
|
|
108
|
+
inverse.unshift({ op: "replace", path: operation.path, value: clone(previous) });
|
|
109
|
+
} else if (operation.op === "add") {
|
|
110
|
+
inverse.unshift({ op: "remove", path: inversePath });
|
|
111
|
+
} else if (operation.op === "replace") {
|
|
112
|
+
inverse.unshift({ op: "replace", path: operation.path, value: clone(previous) });
|
|
113
|
+
} else if (operation.op === "remove") {
|
|
114
|
+
inverse.unshift({ op: "add", path: operation.path, value: clone(previous) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return inverse;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function validateOpfDocument(document, validator = validatePresentation) {
|
|
122
|
+
const result = validator(document);
|
|
123
|
+
const valid = Boolean(result?.valid);
|
|
124
|
+
return {
|
|
125
|
+
valid,
|
|
126
|
+
errors: Array.isArray(result?.errors) ? result.errors : [],
|
|
127
|
+
result
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createEditorSession(input, options = {}) {
|
|
132
|
+
let document = parseInput(input);
|
|
133
|
+
let validation = validateOpfDocument(document, options.validate ?? validatePresentation);
|
|
134
|
+
const undoStack = [];
|
|
135
|
+
const redoStack = [];
|
|
136
|
+
const listeners = new Set();
|
|
137
|
+
const rejectInvalid = Boolean(options.rejectInvalid);
|
|
138
|
+
|
|
139
|
+
function emit(event) {
|
|
140
|
+
const snapshot = editor.snapshot();
|
|
141
|
+
for (const listener of listeners) listener({ ...event, snapshot });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function commitPatch(operations, meta = {}) {
|
|
145
|
+
assertPatchOperations(operations);
|
|
146
|
+
const patches = operations.map(normalizeOperation);
|
|
147
|
+
const before = document;
|
|
148
|
+
const inversePatches = invertJsonPatch(before, patches);
|
|
149
|
+
const next = applyJsonPatch(before, patches);
|
|
150
|
+
const nextValidation = validateOpfDocument(next, options.validate ?? validatePresentation);
|
|
151
|
+
|
|
152
|
+
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
153
|
+
throw new OPFEditorError("invalid-opf-edit", "OPF edit produced an invalid document.", {
|
|
154
|
+
issues: nextValidation.errors,
|
|
155
|
+
patches
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
document = next;
|
|
160
|
+
validation = nextValidation;
|
|
161
|
+
undoStack.push({ patches: clonePatchOperations(patches), inversePatches, meta });
|
|
162
|
+
redoStack.length = 0;
|
|
163
|
+
emit({
|
|
164
|
+
type: "patch",
|
|
165
|
+
patches: clonePatchOperations(patches),
|
|
166
|
+
inversePatches: clonePatchOperations(inversePatches),
|
|
167
|
+
validation,
|
|
168
|
+
meta
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
document: clone(document),
|
|
173
|
+
patches: clonePatchOperations(patches),
|
|
174
|
+
inversePatches: clonePatchOperations(inversePatches),
|
|
175
|
+
validation
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const editor = {
|
|
180
|
+
get document() {
|
|
181
|
+
return clone(document);
|
|
182
|
+
},
|
|
183
|
+
get validation() {
|
|
184
|
+
return validation;
|
|
185
|
+
},
|
|
186
|
+
get canUndo() {
|
|
187
|
+
return undoStack.length > 0;
|
|
188
|
+
},
|
|
189
|
+
get canRedo() {
|
|
190
|
+
return redoStack.length > 0;
|
|
191
|
+
},
|
|
192
|
+
snapshot() {
|
|
193
|
+
return {
|
|
194
|
+
document: clone(document),
|
|
195
|
+
validation,
|
|
196
|
+
canUndo: undoStack.length > 0,
|
|
197
|
+
canRedo: redoStack.length > 0,
|
|
198
|
+
undoDepth: undoStack.length,
|
|
199
|
+
redoDepth: redoStack.length
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
subscribe(listener) {
|
|
203
|
+
if (typeof listener !== "function") {
|
|
204
|
+
throw new OPFEditorError("invalid-listener", "Editor listener must be a function.");
|
|
205
|
+
}
|
|
206
|
+
listeners.add(listener);
|
|
207
|
+
return () => listeners.delete(listener);
|
|
208
|
+
},
|
|
209
|
+
get(path, fallback) {
|
|
210
|
+
return getValueAtPath(document, path, fallback);
|
|
211
|
+
},
|
|
212
|
+
set(path, value, meta = {}) {
|
|
213
|
+
return commitPatch(createValuePatch(document, path, value), {
|
|
214
|
+
...meta,
|
|
215
|
+
path: normalizePatchPath(path)
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
setCatalog(path, catalogKind, id, meta = {}) {
|
|
219
|
+
return setCatalogId(editor, path, catalogKind, id, meta);
|
|
220
|
+
},
|
|
221
|
+
applyPatch: commitPatch,
|
|
222
|
+
undo(meta = {}) {
|
|
223
|
+
const entry = undoStack.pop();
|
|
224
|
+
if (!entry) return null;
|
|
225
|
+
document = applyJsonPatch(document, entry.inversePatches);
|
|
226
|
+
validation = validateOpfDocument(document, options.validate ?? validatePresentation);
|
|
227
|
+
redoStack.push(entry);
|
|
228
|
+
emit({
|
|
229
|
+
type: "undo",
|
|
230
|
+
patches: clonePatchOperations(entry.inversePatches),
|
|
231
|
+
redoPatches: clonePatchOperations(entry.patches),
|
|
232
|
+
validation,
|
|
233
|
+
meta
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
document: clone(document),
|
|
237
|
+
patches: clonePatchOperations(entry.inversePatches),
|
|
238
|
+
redoPatches: clonePatchOperations(entry.patches),
|
|
239
|
+
validation
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
redo(meta = {}) {
|
|
243
|
+
const entry = redoStack.pop();
|
|
244
|
+
if (!entry) return null;
|
|
245
|
+
document = applyJsonPatch(document, entry.patches);
|
|
246
|
+
validation = validateOpfDocument(document, options.validate ?? validatePresentation);
|
|
247
|
+
undoStack.push(entry);
|
|
248
|
+
emit({
|
|
249
|
+
type: "redo",
|
|
250
|
+
patches: clonePatchOperations(entry.patches),
|
|
251
|
+
inversePatches: clonePatchOperations(entry.inversePatches),
|
|
252
|
+
validation,
|
|
253
|
+
meta
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
document: clone(document),
|
|
257
|
+
patches: clonePatchOperations(entry.patches),
|
|
258
|
+
inversePatches: clonePatchOperations(entry.inversePatches),
|
|
259
|
+
validation
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return editor;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function createSvgTraceBinding(root, editor, options = {}) {
|
|
268
|
+
assertElementRoot(root);
|
|
269
|
+
assertEditorSession(editor);
|
|
270
|
+
|
|
271
|
+
const selector = options.selector ?? `[${DATA_OPF_PATH}]`;
|
|
272
|
+
const elements = traceElements(root, selector);
|
|
273
|
+
const listeners = [];
|
|
274
|
+
|
|
275
|
+
function select(element, sourceEvent) {
|
|
276
|
+
const path = opfPathForElement(element);
|
|
277
|
+
const detail = {
|
|
278
|
+
path,
|
|
279
|
+
pointer: opfPathToJsonPointer(path),
|
|
280
|
+
value: editor.get(path),
|
|
281
|
+
element,
|
|
282
|
+
editor,
|
|
283
|
+
sourceEvent
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
options.onSelect?.(detail);
|
|
287
|
+
dispatchOpfEvent(element, SELECT_EVENT, detail);
|
|
288
|
+
return detail;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function commit(elementOrPath, value, meta = {}) {
|
|
292
|
+
const path = typeof elementOrPath === "string" ? elementOrPath : opfPathForElement(elementOrPath);
|
|
293
|
+
return editor.set(path, value, {
|
|
294
|
+
...meta,
|
|
295
|
+
source: meta.source ?? "svg-trace"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const element of elements) {
|
|
300
|
+
element.setAttribute?.("data-opf-bound", "true");
|
|
301
|
+
if (options.interactive !== false) {
|
|
302
|
+
ensureInteractiveTraceElement(element);
|
|
303
|
+
|
|
304
|
+
const onClick = (event) => select(element, event);
|
|
305
|
+
const onKeydown = (event) => {
|
|
306
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
307
|
+
event.preventDefault?.();
|
|
308
|
+
select(element, event);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
element.addEventListener?.("click", onClick);
|
|
313
|
+
element.addEventListener?.("keydown", onKeydown);
|
|
314
|
+
listeners.push([element, "click", onClick], [element, "keydown", onKeydown]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
elements,
|
|
320
|
+
getPath: opfPathForElement,
|
|
321
|
+
getValue(elementOrPath, fallback) {
|
|
322
|
+
return editor.get(typeof elementOrPath === "string" ? elementOrPath : opfPathForElement(elementOrPath), fallback);
|
|
323
|
+
},
|
|
324
|
+
select,
|
|
325
|
+
commit,
|
|
326
|
+
destroy() {
|
|
327
|
+
for (const [element, type, listener] of listeners) {
|
|
328
|
+
element.removeEventListener?.(type, listener);
|
|
329
|
+
}
|
|
330
|
+
listeners.length = 0;
|
|
331
|
+
for (const element of elements) element.removeAttribute?.("data-opf-bound");
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function getCatalogRecords(catalogKind, options = {}) {
|
|
337
|
+
assertCatalogKind(catalogKind);
|
|
338
|
+
const presentationCatalog = options.presentation?.catalogs?.[catalogKind];
|
|
339
|
+
const explicitCatalog = options.catalogs?.[catalogKind] ?? options.catalogSources?.[catalogKind];
|
|
340
|
+
|
|
341
|
+
return firstRecords(presentationCatalog)
|
|
342
|
+
?? firstRecords(explicitCatalog)
|
|
343
|
+
?? firstRecords(bundledCatalogs[catalogKind])
|
|
344
|
+
?? [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function getCatalogOptions(catalogKind, options = {}) {
|
|
348
|
+
return getCatalogRecords(catalogKind, options).map((record) => ({
|
|
349
|
+
id: record.id,
|
|
350
|
+
label: catalogLabel(record),
|
|
351
|
+
record
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function assertCatalogId(catalogKind, id, options = {}) {
|
|
356
|
+
assertCatalogKind(catalogKind);
|
|
357
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
358
|
+
throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog IDs.", {
|
|
359
|
+
catalogKind,
|
|
360
|
+
id
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const optionsById = new Set(getCatalogOptions(catalogKind, options).map((option) => option.id));
|
|
365
|
+
if (!optionsById.has(id)) {
|
|
366
|
+
throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog ID: ${id}.`, {
|
|
367
|
+
catalogKind,
|
|
368
|
+
id
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return id;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
|
|
376
|
+
assertEditorSession(editor);
|
|
377
|
+
const catalogId = assertCatalogId(catalogKind, id, {
|
|
378
|
+
presentation: meta.presentation ?? editor.document,
|
|
379
|
+
catalogs: meta.catalogs,
|
|
380
|
+
catalogSources: meta.catalogSources
|
|
381
|
+
});
|
|
382
|
+
const current = editor.get(path);
|
|
383
|
+
const value = catalogReferenceValue(current, catalogId);
|
|
384
|
+
const { catalogs, catalogSources, presentation, ...commitMeta } = meta;
|
|
385
|
+
return editor.set(path, value, {
|
|
386
|
+
...commitMeta,
|
|
387
|
+
catalogKind,
|
|
388
|
+
catalogId,
|
|
389
|
+
source: commitMeta.source ?? "catalog-control"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function createTextInput(editor, options) {
|
|
394
|
+
assertEditorSession(editor);
|
|
395
|
+
const documentRef = resolveDomDocument(options?.document);
|
|
396
|
+
const input = documentRef.createElement(options?.multiline ? "textarea" : "input");
|
|
397
|
+
const path = options?.path;
|
|
398
|
+
if (!path) throw new OPFEditorError("missing-path", "Text input requires an OPF path.");
|
|
399
|
+
|
|
400
|
+
input.setAttribute(COMPONENT_ATTR, "text-input");
|
|
401
|
+
input.setAttribute(DATA_OPF_PATH, path);
|
|
402
|
+
if (!options?.multiline) input.setAttribute("type", options?.type ?? "text");
|
|
403
|
+
if (options?.label) input.setAttribute("aria-label", options.label);
|
|
404
|
+
|
|
405
|
+
const updateValue = () => {
|
|
406
|
+
const nextValue = editor.get(path, "");
|
|
407
|
+
input.value = nextValue === undefined || nextValue === null ? "" : String(nextValue);
|
|
408
|
+
};
|
|
409
|
+
const onInput = () => {
|
|
410
|
+
editor.set(path, input.value, { source: "text-input" });
|
|
411
|
+
};
|
|
412
|
+
const unsubscribe = editor.subscribe(updateValue);
|
|
413
|
+
|
|
414
|
+
input.addEventListener?.("input", onInput);
|
|
415
|
+
input.destroy = () => {
|
|
416
|
+
input.removeEventListener?.("input", onInput);
|
|
417
|
+
unsubscribe();
|
|
418
|
+
};
|
|
419
|
+
updateValue();
|
|
420
|
+
return input;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function createCatalogSelect(editor, options) {
|
|
424
|
+
assertEditorSession(editor);
|
|
425
|
+
const documentRef = resolveDomDocument(options?.document);
|
|
426
|
+
const select = documentRef.createElement("select");
|
|
427
|
+
const path = options?.path;
|
|
428
|
+
const catalogKind = options?.catalogKind;
|
|
429
|
+
if (!path) throw new OPFEditorError("missing-path", "Catalog select requires an OPF path.");
|
|
430
|
+
assertCatalogKind(catalogKind);
|
|
431
|
+
|
|
432
|
+
select.setAttribute(COMPONENT_ATTR, "catalog-select");
|
|
433
|
+
select.setAttribute(DATA_OPF_PATH, path);
|
|
434
|
+
select.setAttribute("data-opf-catalog-kind", catalogKind);
|
|
435
|
+
if (options?.label) select.setAttribute("aria-label", options.label);
|
|
436
|
+
|
|
437
|
+
for (const option of getCatalogOptions(catalogKind, {
|
|
438
|
+
...options,
|
|
439
|
+
presentation: options?.presentation ?? editor.document
|
|
440
|
+
})) {
|
|
441
|
+
const optionElement = documentRef.createElement("option");
|
|
442
|
+
optionElement.value = option.id;
|
|
443
|
+
optionElement.textContent = option.label;
|
|
444
|
+
select.appendChild(optionElement);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const updateValue = () => {
|
|
448
|
+
const current = editor.get(path);
|
|
449
|
+
select.value = typeof current === "object" && current ? current.id : current ?? "";
|
|
450
|
+
};
|
|
451
|
+
const onChange = () => {
|
|
452
|
+
setCatalogId(editor, path, catalogKind, select.value, {
|
|
453
|
+
catalogs: options?.catalogs,
|
|
454
|
+
catalogSources: options?.catalogSources,
|
|
455
|
+
presentation: options?.presentation,
|
|
456
|
+
source: "catalog-select"
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
const unsubscribe = editor.subscribe(updateValue);
|
|
460
|
+
|
|
461
|
+
select.addEventListener?.("change", onChange);
|
|
462
|
+
select.destroy = () => {
|
|
463
|
+
select.removeEventListener?.("change", onChange);
|
|
464
|
+
unsubscribe();
|
|
465
|
+
};
|
|
466
|
+
updateValue();
|
|
467
|
+
return select;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function parseInput(input) {
|
|
471
|
+
if (typeof input === "string") {
|
|
472
|
+
try {
|
|
473
|
+
return JSON.parse(input);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
throw new OPFEditorError("invalid-json", "OPF input is not valid JSON.", {
|
|
476
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (input instanceof Uint8Array) {
|
|
482
|
+
return parseInput(new TextDecoder().decode(input));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (input && typeof input === "object") return clone(input);
|
|
486
|
+
|
|
487
|
+
throw new OPFEditorError("invalid-input", "OPF input must be a parsed object, JSON string, or Uint8Array.");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function clone(value) {
|
|
491
|
+
if (value === undefined) return undefined;
|
|
492
|
+
return JSON.parse(JSON.stringify(value));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function clonePatchOperations(operations) {
|
|
496
|
+
return operations.map((operation) => clone(operation));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function escapePointerSegment(segment) {
|
|
500
|
+
return String(segment).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function unescapePointerSegment(segment) {
|
|
504
|
+
return segment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function parseJsonPointer(pointer) {
|
|
508
|
+
if (pointer === "") return [];
|
|
509
|
+
if (typeof pointer !== "string" || !pointer.startsWith("/")) {
|
|
510
|
+
throw new OPFEditorError("invalid-json-pointer", "JSON Patch paths must be JSON Pointers.", { path: pointer });
|
|
511
|
+
}
|
|
512
|
+
return pointer.slice(1).split("/").map(unescapePointerSegment);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function normalizePatchPath(path) {
|
|
516
|
+
return typeof path === "string" && path.startsWith("/") ? path : opfPathToJsonPointer(path);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function normalizeOperation(operation) {
|
|
520
|
+
if (!operation || typeof operation !== "object") {
|
|
521
|
+
throw new OPFEditorError("invalid-patch-operation", "JSON Patch operation must be an object.", { operation });
|
|
522
|
+
}
|
|
523
|
+
if (!["add", "replace", "remove"].includes(operation.op)) {
|
|
524
|
+
throw new OPFEditorError("unsupported-patch-operation", `Unsupported JSON Patch operation: ${operation.op}.`, {
|
|
525
|
+
operation
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const normalized = {
|
|
529
|
+
op: operation.op,
|
|
530
|
+
path: normalizePatchPath(operation.path)
|
|
531
|
+
};
|
|
532
|
+
if (operation.op !== "remove") normalized.value = clone(operation.value);
|
|
533
|
+
return normalized;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function assertPatchOperations(operations) {
|
|
537
|
+
if (!Array.isArray(operations)) {
|
|
538
|
+
throw new OPFEditorError("invalid-patch", "JSON Patch must be an array of operations.", { operations });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function readAtPath(document, segments) {
|
|
543
|
+
let current = document;
|
|
544
|
+
for (const segment of segments) {
|
|
545
|
+
if (Array.isArray(current)) {
|
|
546
|
+
if (segment === "-") return MISSING;
|
|
547
|
+
const index = Number(segment);
|
|
548
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) return MISSING;
|
|
549
|
+
current = current[index];
|
|
550
|
+
} else if (current && typeof current === "object" && Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
551
|
+
current = current[segment];
|
|
552
|
+
} else {
|
|
553
|
+
return MISSING;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return current;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function applyJsonPatchOperation(document, operation) {
|
|
560
|
+
const segments = parseJsonPointer(operation.path);
|
|
561
|
+
if (segments.length === 0) {
|
|
562
|
+
if (operation.op === "remove") return undefined;
|
|
563
|
+
return clone(operation.value);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const next = clone(document);
|
|
567
|
+
const parent = readAtPath(next, segments.slice(0, -1));
|
|
568
|
+
const key = segments.at(-1);
|
|
569
|
+
|
|
570
|
+
if (parent === MISSING || parent === null || typeof parent !== "object") {
|
|
571
|
+
throw new OPFEditorError("patch-parent-missing", `Patch parent does not exist for ${operation.path}.`, {
|
|
572
|
+
path: operation.path,
|
|
573
|
+
operation
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (Array.isArray(parent)) applyArrayOperation(parent, key, operation);
|
|
578
|
+
else applyObjectOperation(parent, key, operation);
|
|
579
|
+
|
|
580
|
+
return next;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function insertedPathForAdd(path, previous, document) {
|
|
584
|
+
if (previous !== MISSING || !path.endsWith("/-")) return path;
|
|
585
|
+
const parentPath = path.slice(0, -2);
|
|
586
|
+
const parent = readAtPath(document, parseJsonPointer(parentPath));
|
|
587
|
+
if (!Array.isArray(parent)) return path;
|
|
588
|
+
return `${parentPath}/${parent.length}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function pathParentIsArray(document, path) {
|
|
592
|
+
const segments = parseJsonPointer(path);
|
|
593
|
+
if (segments.length === 0) return false;
|
|
594
|
+
const parent = readAtPath(document, segments.slice(0, -1));
|
|
595
|
+
return Array.isArray(parent);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function applyArrayOperation(parent, key, operation) {
|
|
599
|
+
const index = key === "-" ? parent.length : Number(key);
|
|
600
|
+
if (!Number.isInteger(index) || index < 0 || index > parent.length) {
|
|
601
|
+
throw new OPFEditorError("invalid-array-index", `Invalid array index in patch path ${operation.path}.`, {
|
|
602
|
+
path: operation.path,
|
|
603
|
+
operation
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (operation.op === "add") {
|
|
608
|
+
parent.splice(index, 0, clone(operation.value));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (index >= parent.length) {
|
|
613
|
+
throw new OPFEditorError("patch-path-missing", `Patch path does not exist: ${operation.path}.`, {
|
|
614
|
+
path: operation.path,
|
|
615
|
+
operation
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (operation.op === "replace") parent[index] = clone(operation.value);
|
|
620
|
+
else parent.splice(index, 1);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function applyObjectOperation(parent, key, operation) {
|
|
624
|
+
if (operation.op === "add") {
|
|
625
|
+
parent[key] = clone(operation.value);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!Object.prototype.hasOwnProperty.call(parent, key)) {
|
|
630
|
+
throw new OPFEditorError("patch-path-missing", `Patch path does not exist: ${operation.path}.`, {
|
|
631
|
+
path: operation.path,
|
|
632
|
+
operation
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (operation.op === "replace") parent[key] = clone(operation.value);
|
|
637
|
+
else delete parent[key];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function assertElementRoot(root) {
|
|
641
|
+
if (!root || typeof root !== "object" || typeof root.querySelectorAll !== "function") {
|
|
642
|
+
throw new OPFEditorError("invalid-root", "SVG trace binding requires a root element with querySelectorAll.");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function assertEditorSession(editor) {
|
|
647
|
+
if (!editor || typeof editor.get !== "function" || typeof editor.set !== "function" || typeof editor.subscribe !== "function") {
|
|
648
|
+
throw new OPFEditorError("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function traceElements(root, selector) {
|
|
653
|
+
const elements = Array.from(root.querySelectorAll(selector));
|
|
654
|
+
if (typeof root.matches === "function" && root.matches(selector)) elements.unshift(root);
|
|
655
|
+
return elements;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function opfPathForElement(element) {
|
|
659
|
+
const path = element?.getAttribute?.(DATA_OPF_PATH);
|
|
660
|
+
if (!path) throw new OPFEditorError("missing-opf-path", "Element does not have a data-opf-path attribute.");
|
|
661
|
+
return path;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function ensureInteractiveTraceElement(element) {
|
|
665
|
+
if (!element.hasAttribute?.("tabindex")) element.setAttribute?.("tabindex", "0");
|
|
666
|
+
if (!element.hasAttribute?.("role")) element.setAttribute?.("role", "button");
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function dispatchOpfEvent(element, type, detail) {
|
|
670
|
+
if (typeof element.dispatchEvent !== "function" || typeof globalThis.CustomEvent !== "function") return;
|
|
671
|
+
element.dispatchEvent(new CustomEvent(type, { bubbles: true, detail }));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function assertCatalogKind(catalogKind) {
|
|
675
|
+
if (!catalogKinds.includes(catalogKind)) {
|
|
676
|
+
throw new OPFEditorError("unknown-catalog-kind", `Unknown OPF catalog kind: ${catalogKind}.`, {
|
|
677
|
+
catalogKind
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function firstRecords(value) {
|
|
683
|
+
if (Array.isArray(value)) return value.filter(isCatalogRecord);
|
|
684
|
+
if (Array.isArray(value?.records)) return value.records.filter(isCatalogRecord);
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isCatalogRecord(record) {
|
|
689
|
+
return Boolean(record && typeof record === "object" && typeof record.id === "string");
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function catalogLabel(record) {
|
|
693
|
+
return String(record.name ?? record.title ?? record.label ?? record.id);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function catalogReferenceValue(current, id) {
|
|
697
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
698
|
+
return { ...current, id };
|
|
699
|
+
}
|
|
700
|
+
return id;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function resolveDomDocument(documentRef) {
|
|
704
|
+
const resolved = documentRef ?? globalThis.document;
|
|
705
|
+
if (!resolved || typeof resolved.createElement !== "function") {
|
|
706
|
+
throw new OPFEditorError("missing-document", "DOM components require a document-like object.");
|
|
707
|
+
}
|
|
708
|
+
return resolved;
|
|
709
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EditorSession, CatalogOption } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export interface OPFTextInputProps {
|
|
4
|
+
editor: EditorSession;
|
|
5
|
+
path: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
multiline?: boolean;
|
|
8
|
+
inputProps?: Record<string, unknown>;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OPFCatalogSelectProps {
|
|
13
|
+
editor: EditorSession;
|
|
14
|
+
path: string;
|
|
15
|
+
catalogKind: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
catalogOptions?: CatalogOption[];
|
|
18
|
+
selectProps?: Record<string, unknown>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare function createOPFReactComponents(React: {
|
|
23
|
+
createElement: (...args: unknown[]) => unknown;
|
|
24
|
+
useSyncExternalStore?: (...args: unknown[]) => unknown;
|
|
25
|
+
useState?: (...args: unknown[]) => unknown;
|
|
26
|
+
useEffect?: (...args: unknown[]) => unknown;
|
|
27
|
+
}): {
|
|
28
|
+
useEditorSnapshot(editor: EditorSession): unknown;
|
|
29
|
+
OPFTextInput(props: OPFTextInputProps): unknown;
|
|
30
|
+
OPFCatalogSelect(props: OPFCatalogSelectProps): unknown;
|
|
31
|
+
};
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCatalogOptions,
|
|
3
|
+
getValueAtPath,
|
|
4
|
+
setCatalogId
|
|
5
|
+
} from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function createOPFReactComponents(React) {
|
|
8
|
+
if (!React || typeof React.createElement !== "function") {
|
|
9
|
+
throw new TypeError("createOPFReactComponents requires a React-compatible object.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function useEditorSnapshot(editor) {
|
|
13
|
+
if (typeof React.useSyncExternalStore === "function") {
|
|
14
|
+
return React.useSyncExternalStore(
|
|
15
|
+
(notify) => editor.subscribe(notify),
|
|
16
|
+
() => editor.snapshot(),
|
|
17
|
+
() => editor.snapshot()
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [snapshot, setSnapshot] = React.useState(editor.snapshot());
|
|
22
|
+
React.useEffect(() => editor.subscribe(() => setSnapshot(editor.snapshot())), [editor]);
|
|
23
|
+
return snapshot;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function OPFTextInput({
|
|
27
|
+
editor,
|
|
28
|
+
path,
|
|
29
|
+
label,
|
|
30
|
+
multiline = false,
|
|
31
|
+
inputProps = {},
|
|
32
|
+
...props
|
|
33
|
+
}) {
|
|
34
|
+
const snapshot = useEditorSnapshot(editor);
|
|
35
|
+
const value = getValueAtPath(snapshot.document, path, "");
|
|
36
|
+
const tag = multiline ? "textarea" : "input";
|
|
37
|
+
return React.createElement(tag, {
|
|
38
|
+
...props,
|
|
39
|
+
...inputProps,
|
|
40
|
+
"aria-label": label ?? props["aria-label"] ?? inputProps["aria-label"],
|
|
41
|
+
"data-opf-component": "text-input",
|
|
42
|
+
"data-opf-path": path,
|
|
43
|
+
type: multiline ? undefined : props.type ?? inputProps.type ?? "text",
|
|
44
|
+
value: value === undefined || value === null ? "" : String(value),
|
|
45
|
+
onChange(event) {
|
|
46
|
+
inputProps.onChange?.(event);
|
|
47
|
+
props.onChange?.(event);
|
|
48
|
+
if (!event.defaultPrevented) editor.set(path, event.currentTarget.value, { source: "react-text-input" });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function OPFCatalogSelect({
|
|
54
|
+
editor,
|
|
55
|
+
path,
|
|
56
|
+
catalogKind,
|
|
57
|
+
label,
|
|
58
|
+
catalogOptions,
|
|
59
|
+
selectProps = {},
|
|
60
|
+
...props
|
|
61
|
+
}) {
|
|
62
|
+
const snapshot = useEditorSnapshot(editor);
|
|
63
|
+
const value = getValueAtPath(snapshot.document, path, "");
|
|
64
|
+
const currentId = value && typeof value === "object" ? value.id : value;
|
|
65
|
+
const options = catalogOptions ?? getCatalogOptions(catalogKind, { presentation: snapshot.document });
|
|
66
|
+
|
|
67
|
+
return React.createElement(
|
|
68
|
+
"select",
|
|
69
|
+
{
|
|
70
|
+
...props,
|
|
71
|
+
...selectProps,
|
|
72
|
+
"aria-label": label ?? props["aria-label"] ?? selectProps["aria-label"],
|
|
73
|
+
"data-opf-component": "catalog-select",
|
|
74
|
+
"data-opf-catalog-kind": catalogKind,
|
|
75
|
+
"data-opf-path": path,
|
|
76
|
+
value: currentId ?? "",
|
|
77
|
+
onChange(event) {
|
|
78
|
+
selectProps.onChange?.(event);
|
|
79
|
+
props.onChange?.(event);
|
|
80
|
+
if (!event.defaultPrevented) {
|
|
81
|
+
setCatalogId(editor, path, catalogKind, event.currentTarget.value, { source: "react-catalog-select" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
options.map((option) => React.createElement("option", { key: option.id, value: option.id }, option.label))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
useEditorSnapshot,
|
|
91
|
+
OPFTextInput,
|
|
92
|
+
OPFCatalogSelect
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CatalogOptionsInput, EditorSession, SvgTraceBindingOptions } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export interface OPFTextInputActionParams {
|
|
4
|
+
editor: EditorSession;
|
|
5
|
+
path: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OPFCatalogSelectActionParams extends CatalogOptionsInput {
|
|
10
|
+
editor: EditorSession;
|
|
11
|
+
path: string;
|
|
12
|
+
catalogKind: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OPFTraceActionParams extends SvgTraceBindingOptions {
|
|
17
|
+
editor: EditorSession;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SvelteAction {
|
|
21
|
+
update(params: unknown): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare function opfTextInput(node: any, params: OPFTextInputActionParams): SvelteAction;
|
|
26
|
+
|
|
27
|
+
export declare function opfCatalogSelect(node: any, params: OPFCatalogSelectActionParams): SvelteAction;
|
|
28
|
+
|
|
29
|
+
export declare function opfTrace(node: any, params: OPFTraceActionParams): SvelteAction;
|
package/dist/svelte.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSvgTraceBinding,
|
|
3
|
+
getCatalogOptions,
|
|
4
|
+
setCatalogId
|
|
5
|
+
} from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function opfTextInput(node, params) {
|
|
8
|
+
let cleanup = () => {};
|
|
9
|
+
|
|
10
|
+
function connect(nextParams) {
|
|
11
|
+
cleanup();
|
|
12
|
+
const { editor, path, label } = nextParams;
|
|
13
|
+
node.setAttribute?.("data-opf-component", "text-input");
|
|
14
|
+
node.setAttribute?.("data-opf-path", path);
|
|
15
|
+
if (label) node.setAttribute?.("aria-label", label);
|
|
16
|
+
|
|
17
|
+
const sync = () => {
|
|
18
|
+
const value = editor.get(path, "");
|
|
19
|
+
node.value = value === undefined || value === null ? "" : String(value);
|
|
20
|
+
};
|
|
21
|
+
const onInput = () => editor.set(path, node.value, { source: "svelte-text-input" });
|
|
22
|
+
const unsubscribe = editor.subscribe(sync);
|
|
23
|
+
|
|
24
|
+
node.addEventListener?.("input", onInput);
|
|
25
|
+
sync();
|
|
26
|
+
cleanup = () => {
|
|
27
|
+
node.removeEventListener?.("input", onInput);
|
|
28
|
+
unsubscribe();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
connect(params);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
update: connect,
|
|
36
|
+
destroy() {
|
|
37
|
+
cleanup();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function opfCatalogSelect(node, params) {
|
|
43
|
+
let cleanup = () => {};
|
|
44
|
+
|
|
45
|
+
function connect(nextParams) {
|
|
46
|
+
cleanup();
|
|
47
|
+
const { editor, path, catalogKind, label } = nextParams;
|
|
48
|
+
node.setAttribute?.("data-opf-component", "catalog-select");
|
|
49
|
+
node.setAttribute?.("data-opf-path", path);
|
|
50
|
+
node.setAttribute?.("data-opf-catalog-kind", catalogKind);
|
|
51
|
+
if (label) node.setAttribute?.("aria-label", label);
|
|
52
|
+
replaceOptions(node, getCatalogOptions(catalogKind, {
|
|
53
|
+
catalogs: nextParams.catalogs,
|
|
54
|
+
catalogSources: nextParams.catalogSources,
|
|
55
|
+
presentation: nextParams.presentation ?? editor.document
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const sync = () => {
|
|
59
|
+
const value = editor.get(path);
|
|
60
|
+
node.value = value && typeof value === "object" ? value.id : value ?? "";
|
|
61
|
+
};
|
|
62
|
+
const onChange = () => setCatalogId(editor, path, catalogKind, node.value, {
|
|
63
|
+
catalogs: nextParams.catalogs,
|
|
64
|
+
catalogSources: nextParams.catalogSources,
|
|
65
|
+
presentation: nextParams.presentation,
|
|
66
|
+
source: "svelte-catalog-select"
|
|
67
|
+
});
|
|
68
|
+
const unsubscribe = editor.subscribe(sync);
|
|
69
|
+
|
|
70
|
+
node.addEventListener?.("change", onChange);
|
|
71
|
+
sync();
|
|
72
|
+
cleanup = () => {
|
|
73
|
+
node.removeEventListener?.("change", onChange);
|
|
74
|
+
unsubscribe();
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
connect(params);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
update: connect,
|
|
82
|
+
destroy() {
|
|
83
|
+
cleanup();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function opfTrace(node, params) {
|
|
89
|
+
let binding = createSvgTraceBinding(node, params.editor, params);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
update(nextParams) {
|
|
93
|
+
binding.destroy();
|
|
94
|
+
binding = createSvgTraceBinding(node, nextParams.editor, nextParams);
|
|
95
|
+
},
|
|
96
|
+
destroy() {
|
|
97
|
+
binding.destroy();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function replaceOptions(node, options) {
|
|
103
|
+
while (node.firstChild) node.removeChild(node.firstChild);
|
|
104
|
+
|
|
105
|
+
const documentRef = node.ownerDocument ?? globalThis.document;
|
|
106
|
+
for (const option of options) {
|
|
107
|
+
const optionElement = documentRef.createElement("option");
|
|
108
|
+
optionElement.value = option.id;
|
|
109
|
+
optionElement.textContent = option.label;
|
|
110
|
+
node.appendChild(optionElement);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openpresentation/opf-editor",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": false,
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/OpenPresentation/opf-editor.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/OpenPresentation/opf-editor/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/OpenPresentation/opf-editor#readme",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./react": {
|
|
33
|
+
"types": "./dist/react.d.ts",
|
|
34
|
+
"default": "./dist/react.js"
|
|
35
|
+
},
|
|
36
|
+
"./svelte": {
|
|
37
|
+
"types": "./dist/svelte.d.ts",
|
|
38
|
+
"default": "./dist/svelte.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "node scripts/build.mjs",
|
|
46
|
+
"typecheck": "node --check src/index.js && node --check src/react.js && node --check src/svelte.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/component-smoke.mjs",
|
|
47
|
+
"test": "npm run build && node test/smoke.mjs && node test/component-smoke.mjs",
|
|
48
|
+
"validate": "node scripts/validate-package.mjs",
|
|
49
|
+
"prepack": "npm run build"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@openpresentation/opf": "^0.2.2"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@openpresentation/opf-render": ">=0.0.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@openpresentation/opf-render": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|