@openpresentation/opf-editor 0.7.1 → 0.8.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 +4 -2
- package/dist/color-authoring.js +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -1
- package/dist/presentation-ids.js +68 -0
- package/dist/rich-text-toolbar.js +2 -1
- package/dist/schema-inspector.js +4 -2
- package/dist/transfer.js +3 -8
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs/roadmap-shaping-20260915.md); it is not part of the published runtime.
|
|
4
4
|
|
|
5
|
+
Version 0.8.0 requires `@openpresentation/opf` ^0.11.0, renderer ^0.9.0, and PPTX ^0.9.0. Named ColorRefs (scheme slots, roles, and `var:<id>`) paint through the published renderer and hex-resolve on export. Payload ids share the slide id namespace for pagination and transfer. Native `schemeClr`, theme write, and `p:hf` remain out of scope.
|
|
6
|
+
|
|
5
7
|
Version 0.7.0 and this checkout require Node 24 (`24.x`). Use `.nvmrc` for local development. Earlier published versions retain their original engine declarations. Browser entrypoints remain browser-safe; native application compatibility is verified separately.
|
|
6
8
|
|
|
7
9
|
Version 0.7.0 forwards effective title alignment and shared outline placement options through composition and explicit pagination. Use the same `textMeasurement` and `textRasterPadding` in canvas rendering and export. Accepted rich-text trace origins support selection, caret placement and undo. See the [source contract and limits](https://github.com/OpenPresentation/opf/blob/f94125a1ff95bf0974a055fe1c081d348dad54f2/docs/plans/text-placement.md); native PowerPoint fidelity remains separate.
|
|
@@ -210,7 +212,7 @@ This repo does not require an npm automation token when Trusted Publishing is co
|
|
|
210
212
|
|
|
211
213
|
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 the sibling OPF repo's `docs/dynamic-composition.md` for the complete contract.
|
|
212
214
|
|
|
213
|
-
Version 0.
|
|
215
|
+
Version 0.8.0 requires `@openpresentation/opf@^0.11.0`. The optional renderer peer requires `@openpresentation/opf-render@^0.9.0`. Coordinated playground export uses `@openpresentation/opf-pptx@^0.9.0`. Clean registry installs support the composition APIs without sibling checkouts. For coordinated source development, build OPF and run `node scripts/link-ecosystem.mjs` there; `pnpm test:ecosystem` verifies shared geometry and import/export behavior.
|
|
214
216
|
|
|
215
217
|
## Local interactive demo
|
|
216
218
|
|
|
@@ -249,7 +251,7 @@ Slide insertion does not merge root speakers, organizations, or narrative metada
|
|
|
249
251
|
|
|
250
252
|
### Rich text on the canvas
|
|
251
253
|
|
|
252
|
-
Select rendered rich text to format it, or double-click a rich text block to type. The toolbar supports character styles, point size, font family, hex color, links, scripts, and selected-text replacement. Plain text payloads offer **Format text** during inline editing. **Edit runs** opens the structured fields; `canvas.editProperties(path)` does the same programmatically. Each action is validated and undoable. Version 0.1.1 supports direct mixed-style typing: double-click to type, drag to select, use Format selection for styles, and commit with Done or Ctrl/Cmd+Enter. Escape cancels. Native input and composition events preserve run metadata; the canonical SVG supplies glyph/caret geometry. The whole session commits as one undo step, with draft undo/redo available while typing. Empty-line caret geometry requires renderer 0.1.1 or later; cross-engine and real operating-system IME verification remain open.
|
|
254
|
+
Select rendered rich text to format it, or double-click a rich text block to type. The toolbar supports character styles, point size, font family, hex color, scheme slots/roles, `var:<id>` references, links, scripts, and selected-text replacement. Plain text payloads offer **Format text** during inline editing. **Edit runs** opens the structured fields; `canvas.editProperties(path)` does the same programmatically. Each action is validated and undoable. Version 0.1.1 supports direct mixed-style typing: double-click to type, drag to select, use Format selection for styles, and commit with Done or Ctrl/Cmd+Enter. Escape cancels. Native input and composition events preserve run metadata; the canonical SVG supplies glyph/caret geometry. The whole session commits as one undo step, with draft undo/redo available while typing. Empty-line caret geometry requires renderer 0.1.1 or later; cross-engine and real operating-system IME verification remain open.
|
|
253
255
|
|
|
254
256
|
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.
|
|
255
257
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
2
|
+
|
|
3
|
+
const SCHEME_SLOTS = new Set([
|
|
4
|
+
"accent1", "accent2", "accent3", "accent4", "accent5", "accent6",
|
|
5
|
+
"dark1", "dark2", "light1", "light2", "hyperlink", "followedHyperlink",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
const ROLE_KEYS = new Set([
|
|
9
|
+
"primary", "secondary", "accent", "background", "surface", "text", "textSecondary",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/** Whether a toolbar-authored text color matches documented ColorRef / TextRun forms. */
|
|
13
|
+
export function isAuthoringColorRef(value) {
|
|
14
|
+
if (value === null || value === undefined || value === "") return true;
|
|
15
|
+
if (typeof value !== "string") return false;
|
|
16
|
+
const ref = value.trim();
|
|
17
|
+
if (ref.startsWith("#")) return HEX.test(ref);
|
|
18
|
+
if (ref.startsWith("var:")) return /^var:[a-z][a-z0-9-]*$/.test(ref);
|
|
19
|
+
return SCHEME_SLOTS.has(ref) || ROLE_KEYS.has(ref);
|
|
20
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { paginateSlide } from "@openpresentation/opf/pagination";
|
|
2
|
+
import { collectReservedPresentationIds } from "./presentation-ids.js";
|
|
2
3
|
import { composeSlide, resolveCanvasDimensions, resolveFontFamilies } from "@openpresentation/opf/composition";
|
|
3
4
|
import {
|
|
4
5
|
catalogKinds,
|
|
@@ -142,6 +143,7 @@ export function validateOpfDocument(document, validator = validatePresentation)
|
|
|
142
143
|
return {
|
|
143
144
|
valid,
|
|
144
145
|
errors: Array.isArray(result?.errors) ? result.errors : [],
|
|
146
|
+
warnings: Array.isArray(result?.warnings) ? result.warnings : [],
|
|
145
147
|
result
|
|
146
148
|
};
|
|
147
149
|
}
|
|
@@ -243,7 +245,10 @@ export function createEditorSession(input, options = {}) {
|
|
|
243
245
|
},
|
|
244
246
|
paginateSlide(slideIndex, options = {}, meta = {}) {
|
|
245
247
|
const resolved = resolveCompositionOptions(document, slideIndex, options);
|
|
246
|
-
const pagination = paginateSlide(document.slides[slideIndex], {
|
|
248
|
+
const pagination = paginateSlide(document.slides[slideIndex], {
|
|
249
|
+
...resolved,
|
|
250
|
+
reservedIds: collectReservedPresentationIds(document),
|
|
251
|
+
});
|
|
247
252
|
// Pagination can persist a readability policy without adding a page. Commit
|
|
248
253
|
// that change too, so preview/export use the same minimum that was evaluated.
|
|
249
254
|
if (pagination.slides.length === 1 && jsonEqual(pagination.slides[0], document.slides[slideIndex])) return { change: null, pagination };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Shared slide and content-payload id collection (matches @openpresentation/opf pagination). */
|
|
2
|
+
|
|
3
|
+
const promotedRegionKeys = [
|
|
4
|
+
"left", "center", "right", "left+center", "center+right", "left+center+right",
|
|
5
|
+
"top", "middle", "bottom", "top+middle", "middle+bottom", "top+middle+bottom",
|
|
6
|
+
"top:left", "top:center", "top:right", "top:left+center", "top:center+right", "top:left+center+right",
|
|
7
|
+
"middle:left", "middle:center", "middle:right", "middle:left+center", "middle:center+right", "middle:left+center+right",
|
|
8
|
+
"bottom:left", "bottom:center", "bottom:right", "bottom:left+center", "bottom:center+right", "bottom:left+center+right",
|
|
9
|
+
"top+middle:left", "top+middle:center", "top+middle:right", "top+middle:left+center", "top+middle:center+right", "top+middle:left+center+right",
|
|
10
|
+
"middle+bottom:left", "middle+bottom:center", "middle+bottom:right", "middle+bottom:left+center", "middle+bottom:center+right", "middle+bottom:left+center+right",
|
|
11
|
+
"top+middle+bottom:left", "top+middle+bottom:center", "top+middle+bottom:right", "top+middle+bottom:left+center", "top+middle+bottom:center+right", "top+middle+bottom:left+center+right",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const MAX_COMPOSITION_DEPTH = 8;
|
|
15
|
+
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function visitContentPayloads(slide, slidePath, visit) {
|
|
21
|
+
const walk = (payload, path, depth, ancestors) => {
|
|
22
|
+
visit(payload, path);
|
|
23
|
+
if (!Array.isArray(payload.blocks) || depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(payload)) return;
|
|
24
|
+
const nested = [...ancestors, payload];
|
|
25
|
+
payload.blocks.forEach((block, index) => {
|
|
26
|
+
if (isRecord(block)) walk(block, path ? `${path}.blocks.${index}` : `blocks.${index}`, depth + 1, nested);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
if (Array.isArray(slide?.blocks)) {
|
|
30
|
+
slide.blocks.forEach((block, index) => {
|
|
31
|
+
if (isRecord(block)) walk(block, slidePath ? `${slidePath}.blocks.${index}` : `blocks.${index}`, 0, []);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
for (const key of promotedRegionKeys) {
|
|
35
|
+
const region = slide?.[key];
|
|
36
|
+
if (isRecord(region)) walk(region, slidePath ? `${slidePath}.${key}` : key, 0, []);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function slideIds(slide) {
|
|
41
|
+
const ids = typeof slide?.id === "string" ? [slide.id] : [];
|
|
42
|
+
if (isRecord(slide)) {
|
|
43
|
+
visitContentPayloads(slide, "", (payload) => {
|
|
44
|
+
if (typeof payload.id === "string") ids.push(payload.id);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return ids;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function collectReservedPresentationIds(document) {
|
|
51
|
+
const ids = [];
|
|
52
|
+
for (const slide of document.slides ?? []) ids.push(...slideIds(slide));
|
|
53
|
+
return ids.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function remapNodeId(node, ids) {
|
|
57
|
+
if (typeof node.id !== "string") return;
|
|
58
|
+
const base = node.id;
|
|
59
|
+
let suffix = 2;
|
|
60
|
+
while (ids.has(node.id)) node.id = `${base}-${suffix++}`;
|
|
61
|
+
ids.add(node.id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Remap slide and payload ids when inserting slides (shared document namespace). */
|
|
65
|
+
export function remapSlideTreeIds(slide, ids) {
|
|
66
|
+
remapNodeId(slide, ids);
|
|
67
|
+
visitContentPayloads(slide, "", (payload) => remapNodeId(payload, ids));
|
|
68
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {richTextContent, formatRichTextRange, replaceRichTextRange} from './rich-text.js';
|
|
2
|
+
import { isAuthoringColorRef } from './color-authoring.js';
|
|
2
3
|
|
|
3
4
|
/** Native SVG selection stays on the exact rendered glyphs; no HTML reflow. */
|
|
4
5
|
export function createRichTextToolbar(root, {editor, report, beforeChange, onProperties, onTyping, validateChange, onChange}) {
|
|
@@ -80,7 +81,7 @@ export function createRichTextToolbar(root, {editor, report, beforeChange, onPro
|
|
|
80
81
|
toolbar.append(input);return input;
|
|
81
82
|
}
|
|
82
83
|
const color=field('Text color');
|
|
83
|
-
color.onchange=()=>{try {if(color.value
|
|
84
|
+
color.onchange=()=>{try {if(color.value&&!isAuthoringColorRef(color.value))throw new Error('Use a hex color, scheme slot or role, or var:<id> (for example #2563eb, accent2, text, or var:risk).');apply({color:color.value||null});}catch(error){report(error);}};
|
|
84
85
|
const size=field('Font size (pt)','number');size.min='0.1';size.step='0.5';size.style.width='80px';
|
|
85
86
|
size.onchange=()=>{try{apply({fontSize:size.value?Number(size.value):null});}catch(error){report(error);}};
|
|
86
87
|
const family=field('Font family');family.onchange=()=>{try{apply({fontFamily:family.value||null});}catch(error){report(error);}};
|
package/dist/schema-inspector.js
CHANGED
|
@@ -17,8 +17,10 @@ export function createSchemaInspector(container, {editor,path='',onDraft,onCommi
|
|
|
17
17
|
function check(){
|
|
18
18
|
const validation=validateOpfDocument(draft);
|
|
19
19
|
apply.disabled=!dirty||!validation.valid;
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const warningNote=validation.warnings?.length?` · ${validation.warnings.length} warning${validation.warnings.length===1?'':'s'}`:'';
|
|
21
|
+
status.textContent=dirty?`Draft changes · apply to save${warningNote}`:`No pending changes${warningNote}`;
|
|
22
|
+
const issues=[...(validation.valid?[]:validation.errors.slice(0,3).map(error=>`${error.path||'/'}: ${error.message}`)),...(validation.warnings?.slice(0,2).map(warning=>`${warning.path||'/'}: ${warning.message}`)??[])];
|
|
23
|
+
showError(issues.join('\n'));
|
|
22
24
|
if(validation.valid)try{onDraft?.({document:structuredClone(draft),path:selected,dirty});}catch(error){showError(`Preview: ${error.message}`);onError?.(error);}
|
|
23
25
|
}
|
|
24
26
|
function mutate(operations,redraw=true){draft=applyJsonPatch(draft,operations);dirty=JSON.stringify(draft)!==base;if(redraw)render();check();}
|
package/dist/transfer.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
validateOpfDocument,
|
|
6
6
|
} from "./index.js";
|
|
7
7
|
import { catalogs } from "@openpresentation/opf";
|
|
8
|
+
import { collectReservedPresentationIds, remapSlideTreeIds } from "./presentation-ids.js";
|
|
8
9
|
export const MAX_OPF_BYTES = 20 * 1024 * 1024;
|
|
9
10
|
const clone = (value) => structuredClone(value);
|
|
10
11
|
export function assertOpf(document) {
|
|
@@ -241,14 +242,8 @@ export function prepareOpfImport(
|
|
|
241
242
|
? `asset:${assetMap.get(asset.slice(6))}`
|
|
242
243
|
: asset;
|
|
243
244
|
}
|
|
244
|
-
const ids = new Set(document
|
|
245
|
-
for (const slide of rewritten.slides)
|
|
246
|
-
if (slide.id) {
|
|
247
|
-
const base = slide.id;
|
|
248
|
-
let n = 2;
|
|
249
|
-
while (ids.has(slide.id)) slide.id = `${base}-${n++}`;
|
|
250
|
-
ids.add(slide.id);
|
|
251
|
-
}
|
|
245
|
+
const ids = new Set(collectReservedPresentationIds(document));
|
|
246
|
+
for (const slide of rewritten.slides) remapSlideTreeIds(slide, ids);
|
|
252
247
|
const index = Math.max(0, Math.min(document.slides.length, slideIndex + 1));
|
|
253
248
|
document.slides.splice(index, 0, ...rewritten.slides);
|
|
254
249
|
assertOpf(document);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
"sideEffects": false,
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build": "node scripts/build.mjs",
|
|
86
|
-
"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 && node --check src/canvas.js && node --check src/canvas-fields.js && node --check test/canvas-fields.mjs && node --check test/browser-canvas.mjs && node --check src/transfer.js && node --check src/galleries.js && node --check examples/transfer-controls.js && node --check src/schema.js && node --check src/schema-inspector.js && node --check test/rich-table-browser.mjs && node --check test/styled-table.mjs && node --check test/styled-table-browser.mjs && node --check examples/pptx-controls.js && node --check scripts/build-playground.mjs && node --check test/playground-pptx.mjs && node --check src/text-input.js && node --check src/rich-text.js && node --check src/rich-text-input.js && node --check test/rich-text.mjs && node --check test/rich-input-browser.mjs && node --check src/json-editor.js && node --check src/json-menu.js && node --check src/json-options.js && node --check src/json-source.js",
|
|
87
|
-
"test": "npm run build && node test/accepted-text.mjs && node test/smoke.mjs && node test/component-smoke.mjs && node test/canvas-fields.mjs && node test/transfer.mjs && node test/schema.mjs && node test/rich-text.mjs && node test/layout.mjs && node test/blocks.mjs && node test/styled-table.mjs && node test/json-source.mjs && node test/json-options.mjs",
|
|
86
|
+
"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 && node --check src/canvas.js && node --check src/canvas-fields.js && node --check test/canvas-fields.mjs && node --check test/browser-canvas.mjs && node --check src/transfer.js && node --check src/presentation-ids.js && node --check src/color-authoring.js && node --check test/presentation-ids.mjs && node --check src/galleries.js && node --check examples/transfer-controls.js && node --check src/schema.js && node --check src/schema-inspector.js && node --check test/rich-table-browser.mjs && node --check test/styled-table.mjs && node --check test/styled-table-browser.mjs && node --check examples/pptx-controls.js && node --check scripts/build-playground.mjs && node --check test/playground-pptx.mjs && node --check src/text-input.js && node --check src/rich-text.js && node --check src/rich-text-input.js && node --check src/rich-text-toolbar.js && node --check test/rich-text.mjs && node --check test/rich-input-browser.mjs && node --check src/json-editor.js && node --check src/json-menu.js && node --check src/json-options.js && node --check src/json-source.js",
|
|
87
|
+
"test": "npm run build && node test/accepted-text.mjs && node test/smoke.mjs && node test/component-smoke.mjs && node test/canvas-fields.mjs && node test/transfer.mjs && node test/presentation-ids.mjs && node test/schema.mjs && node test/rich-text.mjs && node test/layout.mjs && node test/blocks.mjs && node test/styled-table.mjs && node test/json-source.mjs && node test/json-options.mjs",
|
|
88
88
|
"validate": "node scripts/validate-package.mjs",
|
|
89
89
|
"test:packed": "node test/packed-install.mjs",
|
|
90
90
|
"prepack": "npm run build",
|
|
@@ -106,11 +106,11 @@
|
|
|
106
106
|
"@codemirror/state": "6.7.4",
|
|
107
107
|
"@codemirror/view": "6.43.11",
|
|
108
108
|
"@lezer/highlight": "1.2.3",
|
|
109
|
-
"@openpresentation/opf": "^0.
|
|
109
|
+
"@openpresentation/opf": "^0.11.0",
|
|
110
110
|
"jsonc-parser": "3.3.1"
|
|
111
111
|
},
|
|
112
112
|
"peerDependencies": {
|
|
113
|
-
"@openpresentation/opf-render": "^0.
|
|
113
|
+
"@openpresentation/opf-render": "^0.9.0"
|
|
114
114
|
},
|
|
115
115
|
"peerDependenciesMeta": {
|
|
116
116
|
"@openpresentation/opf-render": {
|
|
@@ -118,8 +118,8 @@
|
|
|
118
118
|
}
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
121
|
-
"@openpresentation/opf-pptx": "0.
|
|
122
|
-
"@openpresentation/opf-render": "0.
|
|
121
|
+
"@openpresentation/opf-pptx": "^0.9.0",
|
|
122
|
+
"@openpresentation/opf-render": "^0.9.0",
|
|
123
123
|
"esbuild": "0.28.2",
|
|
124
124
|
"playwright": "1.63.0"
|
|
125
125
|
}
|