From 8ee4e269590747ef01043373077c44522bd1ae79 Mon Sep 17 00:00:00 2001 From: Jedr Blaszyk Date: Wed, 4 Mar 2026 14:55:42 -0800 Subject: [PATCH] feat: add minimal Vue package harness with working editor Implements the Vue wrapper for @eigenpal/docx-core with a working dev server (bun run dev:vue). Includes useDocxEditor composable, DocxEditorVue component, BasicToolbar, renderAsync, and a demo app that loads sample.docx. Contributors can extend with selection overlay, drag-to-select, and additional toolbar features. Co-Authored-By: Claude Opus 4.6 --- bun.lock | 7 +- examples/vue/public/sample.docx | Bin 0 -> 39238 bytes examples/vue/src/App.vue | 239 +++++++++-- .../add-vue-package-harness/.openspec.yaml | 2 + .../changes/add-vue-package-harness/design.md | 76 ++++ .../add-vue-package-harness/proposal.md | 28 ++ .../specs/vue-editor-component/spec.md | 79 ++++ .../changes/add-vue-package-harness/tasks.md | 36 ++ package.json | 5 +- packages/vue/src/components/BasicToolbar.vue | 94 +++++ packages/vue/src/components/DocxEditorVue.vue | 162 ++++++++ packages/vue/src/composables/useDocxEditor.ts | 393 ++++++++++++++++++ packages/vue/src/index.ts | 10 +- packages/vue/src/renderAsync.ts | 71 +++- packages/vue/src/shims-vue.d.ts | 6 + 15 files changed, 1156 insertions(+), 52 deletions(-) create mode 100644 examples/vue/public/sample.docx create mode 100644 openspec/changes/add-vue-package-harness/.openspec.yaml create mode 100644 openspec/changes/add-vue-package-harness/design.md create mode 100644 openspec/changes/add-vue-package-harness/proposal.md create mode 100644 openspec/changes/add-vue-package-harness/specs/vue-editor-component/spec.md create mode 100644 openspec/changes/add-vue-package-harness/tasks.md create mode 100644 packages/vue/src/components/BasicToolbar.vue create mode 100644 packages/vue/src/components/DocxEditorVue.vue create mode 100644 packages/vue/src/composables/useDocxEditor.ts create mode 100644 packages/vue/src/shims-vue.d.ts diff --git a/bun.lock b/bun.lock index 1d046530..3ab10f55 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-vue": "^6.0.4", "autoprefixer": "^10.4.17", "class-variance-authority": "^0.7.0", "eslint": "^9.39.2", @@ -32,6 +33,7 @@ "tsup": "^8.0.1", "typescript": "^5.3.3", "vite": "^7.3.1", + "vue": "^3.5.29", }, }, "packages/core": { @@ -41,7 +43,6 @@ "docx-editor-mcp": "./dist/mcp-cli.js", }, "dependencies": { - "clsx": "^2.1.0", "docxtemplater": "^3.50.0", "jszip": "^3.10.1", "pizzip": "^3.1.7", @@ -393,6 +394,8 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, ""], + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="], + "@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="], "@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="], @@ -1155,6 +1158,8 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, ""], + "@vitejs/plugin-vue/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, ""], diff --git a/examples/vue/public/sample.docx b/examples/vue/public/sample.docx new file mode 100644 index 0000000000000000000000000000000000000000..a18bf1861eca43200d4bb6f93e46e9b74ca05608 GIT binary patch literal 39238 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

^UY^>2BK5w)UzAR3Ob#~#nF=wd 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 HCCGmO+lCvf literal 0 HcmV?d00001 diff --git a/examples/vue/src/App.vue b/examples/vue/src/App.vue index 847ea039..ac0c2da9 100644 --- a/examples/vue/src/App.vue +++ b/examples/vue/src/App.vue @@ -1,59 +1,226 @@ - diff --git a/openspec/changes/add-vue-package-harness/.openspec.yaml b/openspec/changes/add-vue-package-harness/.openspec.yaml new file mode 100644 index 00000000..5aae5cfa --- /dev/null +++ b/openspec/changes/add-vue-package-harness/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-04 diff --git a/openspec/changes/add-vue-package-harness/design.md b/openspec/changes/add-vue-package-harness/design.md new file mode 100644 index 00000000..bb5a39a2 --- /dev/null +++ b/openspec/changes/add-vue-package-harness/design.md @@ -0,0 +1,76 @@ +## Context + +The monorepo already has: + +- `packages/vue/` — a scaffold with `renderAsync` (throws "not implemented") and plugin types +- `examples/vue/` — a Vite app with `@vitejs/plugin-vue`, port 5174, alias config pointing at source — but shows a static "coming soon" page +- `packages/core/` — framework-agnostic core with `EditorCoordinator`, managers, ProseMirror extensions, layout-painter, and the full parsing/serialization pipeline +- `packages/react/` — the reference implementation with `DocxEditor`, `PagedEditor`, `HiddenProseMirror`, toolbar, and hooks + +The React editor's architecture: `DocxEditor` (orchestrator) → `PagedEditor` (visible pages + selection mapping) → `HiddenProseMirror` (off-screen PM instance). All rendering goes through `layout-painter` which is pure DOM manipulation (framework-agnostic). + +## Goals / Non-Goals + +**Goals:** + +- Add `bun run dev:vue` to root package.json, starting Vue example on port 5174 +- Create a minimal `DocxEditorVue` component that renders a DOCX with toolbar and basic editing +- Update `examples/vue/App.vue` to a working demo (open file, new document, save/download) +- Keep it minimal — a contributor-friendly harness, not feature parity with React + +**Non-Goals:** + +- Full feature parity with the React editor (find/replace, plugins, context menus, zoom control, ruler) +- Vue-specific implementations of all React hooks (useAutoSave, useTableSelection, etc.) +- Publishing `@eigenpal/docx-editor-vue` to npm +- Tests for the Vue package (can be added by contributors later) +- Mobile responsive layout in the Vue demo + +## Decisions + +### 1. Wrap layout-painter directly, not through React components + +The React editor's `PagedEditor` and `HiddenProseMirror` are React components that manage DOM directly. Since `layout-painter` is pure DOM manipulation and ProseMirror manages its own DOM, the Vue component can: + +- Create a hidden PM view in `onMounted` +- Use `layout-painter/renderPage.ts` to paint visible pages into a container div +- Handle click-to-position mapping same as React does + +**Alternative considered:** Port React components to Vue line-by-line. Rejected — too much work for a harness, and contributors should design Vue-idiomatic patterns. + +### 2. Single-file component approach + +One `DocxEditorVue.vue` SFC that: + +- Accepts props: `documentBuffer`, `document`, `showToolbar`, `readOnly`, `initialZoom` +- Emits: `change`, `error`, `fontsLoaded` +- Exposes ref methods: `save()`, `getDocument()`, `focus()` +- Uses `@eigenpal/docx-core` imports for parsing, PM setup, and layout painting + +This keeps the harness self-contained. Contributors can later extract composables. + +### 3. Reuse the React toolbar as-is (skip for minimal harness) + +The toolbar is a complex React component. For the minimal harness, we'll render the ProseMirror menubar plugin or skip toolbar entirely and let the hidden PM instance handle keyboard shortcuts. Contributors can build a Vue toolbar later. + +**Decision:** Include a basic Vue toolbar with essential formatting (bold, italic, underline, alignment) using direct ProseMirror commands. This gives contributors a working pattern to extend. + +### 4. Port structure mirrors React but stays minimal + +``` +packages/vue/src/ + components/ + DocxEditorVue.vue — Main editor component + BasicToolbar.vue — Minimal toolbar (B/I/U, alignment) + composables/ + useDocxEditor.ts — Core editor lifecycle (parse, PM, layout-painter) + index.ts — Updated exports + renderAsync.ts — Implement using DocxEditorVue + plugin-api/types.ts — Unchanged +``` + +## Risks / Trade-offs + +- **[Risk] Layout-painter integration may need React-specific assumptions removed** → Mitigation: layout-painter is pure DOM; the React coupling is only in PagedEditor.tsx which we're not using. We'll call the same functions directly. +- **[Risk] Contributor confusion about what's "done" vs "harness"** → Mitigation: Clear TODO comments and a README section listing what's implemented vs what needs work. +- **[Risk] ProseMirror CSS may clash with Vue scoped styles** → Mitigation: Use unscoped styles or import the existing editor.css from core. diff --git a/openspec/changes/add-vue-package-harness/proposal.md b/openspec/changes/add-vue-package-harness/proposal.md new file mode 100644 index 00000000..b924038e --- /dev/null +++ b/openspec/changes/add-vue-package-harness/proposal.md @@ -0,0 +1,28 @@ +## Why + +The Vue package (`packages/vue`) and its example (`examples/vue`) exist as scaffolds with placeholder content — `renderAsync` throws "not yet implemented" and the example shows a static "coming soon" page. There's no `dev:vue` script, so contributors can't easily spin up the Vue dev server. Adding a minimal working harness with a `bun run dev:vue` command lets Vue contributors start iterating immediately without figuring out the project structure. + +## What Changes + +- Add `dev:vue` script to root `package.json` to start the Vue example dev server on port 5174 +- Implement a minimal `DocxEditor` Vue component in `packages/vue` that wraps `@eigenpal/docx-core` to render a DOCX file with toolbar and basic editing +- Update `examples/vue/src/App.vue` from placeholder to a working demo (file open, new, save) mirroring the React Vite example's functionality +- Wire up the Vue example to use the new component + +## Capabilities + +### New Capabilities + +- `vue-editor-component`: Minimal Vue 3 component wrapping @eigenpal/docx-core that provides DOCX rendering and editing with toolbar support + +### Modified Capabilities + +_None — this is a new integration, no existing spec-level behavior changes._ + +## Impact + +- **packages/vue/**: New component files (DocxEditor.vue or composable), updated `index.ts` exports +- **examples/vue/**: Updated App.vue with working editor demo, possibly new dependencies +- **root package.json**: New `dev:vue` script +- **Dependencies**: `vue` (already a peer dep), may need `@eigenpal/docx-core` manager APIs +- **No breaking changes**: The existing placeholder exports remain; new component is additive diff --git a/openspec/changes/add-vue-package-harness/specs/vue-editor-component/spec.md b/openspec/changes/add-vue-package-harness/specs/vue-editor-component/spec.md new file mode 100644 index 00000000..d075c8f9 --- /dev/null +++ b/openspec/changes/add-vue-package-harness/specs/vue-editor-component/spec.md @@ -0,0 +1,79 @@ +## ADDED Requirements + +### Requirement: Dev server script for Vue + +The monorepo root `package.json` SHALL include a `dev:vue` script that starts the Vue example dev server on port 5174. + +#### Scenario: Start Vue dev server + +- **WHEN** a contributor runs `bun run dev:vue` from the monorepo root +- **THEN** Vite starts serving the Vue example at `http://localhost:5174` + +### Requirement: DocxEditorVue component renders DOCX + +The `packages/vue` package SHALL export a `DocxEditorVue` Vue 3 component that accepts a DOCX ArrayBuffer and renders it as editable pages using `@eigenpal/docx-core`. + +#### Scenario: Render a DOCX buffer + +- **WHEN** `DocxEditorVue` receives a `documentBuffer` prop containing a valid DOCX ArrayBuffer +- **THEN** the component parses the buffer, creates a ProseMirror editor, and paints visible pages using layout-painter + +#### Scenario: Render an empty document + +- **WHEN** `DocxEditorVue` receives a `document` prop with a Document model object +- **THEN** the component renders the document without needing a DOCX buffer + +### Requirement: Basic editing via ProseMirror + +The `DocxEditorVue` component SHALL support text editing through the hidden ProseMirror instance, handling keyboard input (typing, backspace, enter, undo/redo). + +#### Scenario: Type text into editor + +- **WHEN** the user clicks on the visible page area and types text +- **THEN** the hidden ProseMirror instance receives the input and layout-painter re-renders the visible pages with the new content + +### Requirement: Save document as DOCX + +The `DocxEditorVue` component SHALL expose a `save()` method via template ref that serializes the current editor state back to a DOCX Blob. + +#### Scenario: Save edited document + +- **WHEN** the host application calls `editorRef.save()` +- **THEN** the component returns a Promise resolving to a Blob containing the DOCX file + +### Requirement: Basic toolbar + +The `packages/vue` package SHALL include a `BasicToolbar.vue` component providing essential formatting controls: bold, italic, underline, and text alignment (left, center, right). + +#### Scenario: Apply bold formatting + +- **WHEN** the user selects text and clicks the Bold toolbar button +- **THEN** the selected text is toggled bold via the ProseMirror command + +### Requirement: Vue example app with file operations + +The `examples/vue/App.vue` SHALL provide a working demo with: open DOCX file, create new document, and save/download document. + +#### Scenario: Open a DOCX file + +- **WHEN** the user clicks "Open DOCX" and selects a .docx file +- **THEN** the file is loaded into the DocxEditorVue component and rendered + +#### Scenario: Create a new empty document + +- **WHEN** the user clicks "New" +- **THEN** the editor resets to an empty document + +#### Scenario: Save and download + +- **WHEN** the user clicks "Save" +- **THEN** the current document is serialized and downloaded as a .docx file + +### Requirement: Sample document on load + +The Vue example app SHALL load a sample DOCX file on initial page load, matching the React example behavior. + +#### Scenario: Initial load with sample + +- **WHEN** the Vue example page loads +- **THEN** the app fetches `/sample.docx` from the public directory and renders it in the editor diff --git a/openspec/changes/add-vue-package-harness/tasks.md b/openspec/changes/add-vue-package-harness/tasks.md new file mode 100644 index 00000000..a83b13d7 --- /dev/null +++ b/openspec/changes/add-vue-package-harness/tasks.md @@ -0,0 +1,36 @@ +## 1. Project Setup + +- [x] 1.1 Add `dev:vue` script to root `package.json`: `"dev:vue": "cd examples/vue && bun run dev"` +- [x] 1.2 Add `vue` and `@vitejs/plugin-vue` to Vue example devDependencies if missing, run `bun install` +- [x] 1.3 Copy `sample.docx` from `examples/vite/public/` to `examples/vue/public/` so the demo can load it on startup + +## 2. Core Composable + +- [x] 2.1 Create `packages/vue/src/composables/useDocxEditor.ts` — composable that manages the editor lifecycle: parse DOCX buffer → build ProseMirror state → create hidden PM view → paint pages via layout-painter → handle re-renders on state change +- [x] 2.2 Expose from composable: `editorView` ref, `save()` method, `destroy()` cleanup, `isReady` ref, `parseError` ref + +## 3. Vue Components + +- [x] 3.1 Create `packages/vue/src/components/DocxEditorVue.vue` — main editor SFC that uses `useDocxEditor`, renders hidden PM container + visible pages container, handles click-to-position mapping for selection +- [x] 3.2 Create `packages/vue/src/components/BasicToolbar.vue` — minimal toolbar with bold, italic, underline, and alignment buttons using ProseMirror commands +- [x] 3.3 Wire toolbar to editor view: pass `editorView` to BasicToolbar, dispatch commands on button click + +## 4. Package Exports + +- [x] 4.1 Update `packages/vue/src/index.ts` to export `DocxEditorVue` component and `useDocxEditor` composable +- [x] 4.2 Implement `renderAsync.ts` to mount `DocxEditorVue` into a container element using `createApp` + +## 5. Example App + +- [x] 5.1 Rewrite `examples/vue/src/App.vue` with working demo: header with Open/New/Save buttons, file name display, DocxEditorVue component +- [x] 5.2 Implement file open handler (file input → ArrayBuffer → pass to editor) +- [x] 5.3 Implement new document handler (createEmptyDocument from core → pass to editor) +- [x] 5.4 Implement save/download handler (call editor ref save → create download link) +- [x] 5.5 Load `sample.docx` on initial page load via fetch + +## 6. Verification + +- [x] 6.1 Run `bun run dev:vue`, verify page loads at localhost:5174 with sample document rendered +- [ ] 6.2 Verify typing, bold/italic/underline, and alignment work (requires manual browser testing) +- [ ] 6.3 Verify open file, new document, and save/download work (requires manual browser testing) +- [x] 6.4 Run `bun run typecheck` to confirm no type errors diff --git a/package.json b/package.json index ffc8f080..22d12ed3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev:nextjs": "cd examples/nextjs && npm run dev", "dev:remix": "cd examples/remix && npm run dev", "dev:astro": "cd examples/astro && npm run dev", + "dev:vue": "cd examples/vue && bun run dev", "dev:demo": "bash examples/dev-all.sh", "build": "bun run --filter '@eigenpal/docx-core' build && bun run --filter '@eigenpal/docx-js-editor' build", "build:demo": "vite build --config examples/vite/vite.config.ts", @@ -38,6 +39,7 @@ "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-vue": "^6.0.4", "autoprefixer": "^10.4.17", "class-variance-authority": "^0.7.0", "eslint": "^9.39.2", @@ -54,7 +56,8 @@ "tailwindcss-animate": "^1.0.7", "tsup": "^8.0.1", "typescript": "^5.3.3", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vue": "^3.5.29" }, "keywords": [ "docx", diff --git a/packages/vue/src/components/BasicToolbar.vue b/packages/vue/src/components/BasicToolbar.vue new file mode 100644 index 00000000..aac4e29c --- /dev/null +++ b/packages/vue/src/components/BasicToolbar.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/packages/vue/src/components/DocxEditorVue.vue b/packages/vue/src/components/DocxEditorVue.vue new file mode 100644 index 00000000..a949d084 --- /dev/null +++ b/packages/vue/src/components/DocxEditorVue.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/packages/vue/src/composables/useDocxEditor.ts b/packages/vue/src/composables/useDocxEditor.ts new file mode 100644 index 00000000..bd1ee64c --- /dev/null +++ b/packages/vue/src/composables/useDocxEditor.ts @@ -0,0 +1,393 @@ +/** + * useDocxEditor — Vue composable for the DOCX editor lifecycle. + * + * Manages: DOCX parsing → ProseMirror state → layout pipeline → DOM painting. + * This is the Vue equivalent of PagedEditor + HiddenProseMirror from the React package. + */ + +import { ref, onBeforeUnmount, shallowRef, type Ref } from 'vue'; +import { EditorState, type Transaction, type Plugin } from 'prosemirror-state'; +import { EditorView } from 'prosemirror-view'; + +// Core imports — these all resolve through Vite aliases to packages/core/src/ +import { parseDocx } from '@eigenpal/docx-core/docx/parser'; +import { toProseDoc, createEmptyDoc } from '@eigenpal/docx-core/prosemirror/conversion'; +import { fromProseDoc } from '@eigenpal/docx-core/prosemirror/conversion/fromProseDoc'; +import { ExtensionManager } from '@eigenpal/docx-core/prosemirror/extensions/ExtensionManager'; +import { createStarterKit } from '@eigenpal/docx-core/prosemirror/extensions/StarterKit'; +import { toFlowBlocks } from '@eigenpal/docx-core/layout-bridge/toFlowBlocks'; +import { measureParagraph } from '@eigenpal/docx-core/layout-bridge/measuring'; +import { layoutDocument } from '@eigenpal/docx-core/layout-engine'; +import { renderPages } from '@eigenpal/docx-core/layout-painter/renderPage'; +import type { + FlowBlock, + Measure, + ParagraphBlock, + TableBlock, + TableMeasure, + ImageBlock, + PageMargins, +} from '@eigenpal/docx-core/layout-engine/types'; +import type { BlockLookup } from '@eigenpal/docx-core/layout-painter'; +import type { Document, SectionProperties } from '@eigenpal/docx-core/types/document'; + +// ProseMirror CSS — must be imported for the hidden editor to work +import 'prosemirror-view/style/prosemirror.css'; +import '@eigenpal/docx-core/prosemirror/editor.css'; + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const DEFAULT_PAGE_WIDTH = 816; // 8.5in at 96dpi +const DEFAULT_PAGE_HEIGHT = 1056; // 11in at 96dpi +const DEFAULT_MARGINS: PageMargins = { top: 96, right: 96, bottom: 96, left: 96 }; +const DEFAULT_PAGE_GAP = 24; + +// ============================================================================ +// HELPERS +// ============================================================================ + +function twipsToPixels(twips: number): number { + return Math.round((twips / 1440) * 96); +} + +function getPageSize(sp: SectionProperties | null | undefined) { + return { + w: sp?.pageWidth ? twipsToPixels(sp.pageWidth) : DEFAULT_PAGE_WIDTH, + h: sp?.pageHeight ? twipsToPixels(sp.pageHeight) : DEFAULT_PAGE_HEIGHT, + }; +} + +function getMargins(sp: SectionProperties | null | undefined): PageMargins { + return { + top: sp?.marginTop ? twipsToPixels(sp.marginTop) : DEFAULT_MARGINS.top, + right: sp?.marginRight ? twipsToPixels(sp.marginRight) : DEFAULT_MARGINS.right, + bottom: sp?.marginBottom ? twipsToPixels(sp.marginBottom) : DEFAULT_MARGINS.bottom, + left: sp?.marginLeft ? twipsToPixels(sp.marginLeft) : DEFAULT_MARGINS.left, + }; +} + +/** + * Simplified block measurement (no floating zone support for the minimal harness). + * Contributors can port the full measureBlocks from PagedEditor later. + */ +function measureBlock(block: FlowBlock, contentWidth: number): Measure { + switch (block.kind) { + case 'paragraph': + return measureParagraph(block as ParagraphBlock, contentWidth); + + case 'table': { + const tb = block as TableBlock; + let columnWidths = tb.columnWidths ?? []; + if (columnWidths.length === 0 && tb.rows.length > 0) { + const colCount = tb.rows[0].cells.reduce((sum, cell) => sum + (cell.colSpan ?? 1), 0); + const equalWidth = contentWidth / Math.max(1, colCount); + columnWidths = Array(colCount).fill(equalWidth); + } + + const rows = tb.rows.map((row) => { + let colIdx = 0; + const cells = row.cells.map((cell) => { + const colSpan = cell.colSpan ?? 1; + let cellWidth = 0; + for (let c = 0; c < colSpan && colIdx + c < columnWidths.length; c++) { + cellWidth += columnWidths[colIdx + c] ?? 0; + } + if (cellWidth === 0) cellWidth = cell.width ?? 100; + colIdx += colSpan; + + const cellContentWidth = Math.max(1, cellWidth - 14); // ~7px padding each side + const blocks = cell.blocks.map((b) => measureBlock(b, cellContentWidth)); + const height = + blocks.reduce( + (h, m) => h + ('totalHeight' in m ? (m as { totalHeight: number }).totalHeight : 0), + 0 + ) + 2; // padding + + return { blocks, width: cellWidth, height, colSpan: cell.colSpan, rowSpan: cell.rowSpan }; + }); + + const maxHeight = Math.max(24, ...cells.map((c) => c.height)); + return { cells, height: maxHeight }; + }); + + const totalWidth = columnWidths.reduce((s, w) => s + w, 0) || contentWidth; + const totalHeight = rows.reduce((h, r) => h + r.height, 0); + return { kind: 'table', rows, columnWidths, totalWidth, totalHeight } as TableMeasure; + } + + case 'image': { + const ib = block as ImageBlock; + return { kind: 'image', width: ib.width ?? 100, height: ib.height ?? 100 }; + } + + case 'pageBreak': + return { kind: 'pageBreak' }; + + case 'columnBreak': + return { kind: 'columnBreak' }; + + case 'sectionBreak': + return { kind: 'sectionBreak' }; + + default: + return { kind: 'paragraph', lines: [], totalHeight: 0 }; + } +} + +function measureBlocks(blocks: FlowBlock[], contentWidth: number): Measure[] { + return blocks.map((b) => measureBlock(b, contentWidth)); +} + +// ============================================================================ +// COMPOSABLE +// ============================================================================ + +export interface UseDocxEditorOptions { + /** Container element for the hidden ProseMirror editor */ + hiddenContainer: Ref; + /** Container element for the visible pages */ + pagesContainer: Ref; + /** Whether the editor is read-only */ + readOnly?: boolean; + /** Page gap in pixels */ + pageGap?: number; + /** Callback on document change */ + onChange?: (doc: Document) => void; + /** Callback on error */ + onError?: (error: Error) => void; +} + +export function useDocxEditor(options: UseDocxEditorOptions) { + const { + hiddenContainer, + pagesContainer, + readOnly = false, + pageGap = DEFAULT_PAGE_GAP, + onChange, + onError, + } = options; + + // State + const document = shallowRef(null); + const editorView = shallowRef(null); + const isReady = ref(false); + const parseError = ref(null); + + // Extension manager (created once) + let extensionManager: ExtensionManager | null = null; + + function getExtensionManager(): ExtensionManager { + if (!extensionManager) { + extensionManager = new ExtensionManager(createStarterKit()); + extensionManager.buildSchema(); + extensionManager.initializeRuntime(); + } + return extensionManager; + } + + // ======================================================================== + // Layout pipeline + // ======================================================================== + + function runLayoutPipeline(state: EditorState) { + const container = pagesContainer.value; + if (!container || !document.value) return; + + const sp = document.value.package?.document?.finalSectionProperties ?? null; + const pageSize = getPageSize(sp); + const margins = getMargins(sp); + const contentWidth = pageSize.w - margins.left - margins.right; + const pageContentHeight = pageSize.h - margins.top - margins.bottom; + const theme = document.value.package?.theme ?? null; + + try { + // Step 1: PM doc → flow blocks + const blocks = toFlowBlocks(state.doc, { theme, pageContentHeight }); + + // Step 2: Measure blocks + const measures = measureBlocks(blocks, contentWidth); + + // Step 3: Layout + const newLayout = layoutDocument(blocks, measures, { pageSize, margins }); + + // Step 4: Build block lookup and paint + const blockLookup: BlockLookup = new Map(); + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + const measure = measures[i]; + if (block && measure) { + blockLookup.set(String(block.id), { block, measure }); + } + } + + renderPages(newLayout.pages, container, { + pageGap, + showShadow: true, + pageBackground: '#fff', + blockLookup, + theme, + } as Parameters[2]); + } catch (err) { + console.error('[useDocxEditor] Layout pipeline error:', err); + onError?.(err instanceof Error ? err : new Error(String(err))); + } + } + + // ======================================================================== + // ProseMirror setup + // ======================================================================== + + function createEditorView() { + const host = hiddenContainer.value; + if (!host) return; + + const mgr = getExtensionManager(); + const doc = document.value + ? toProseDoc(document.value, { + styles: document.value.package?.styles ?? undefined, + }) + : createEmptyDoc(); + + const plugins: Plugin[] = [...(mgr.getPlugins() ?? [])]; + + const state = EditorState.create({ + doc, + schema: mgr.getSchema(), + plugins, + }); + + const view = new EditorView(host, { + state, + editable: () => !readOnly, + dispatchTransaction(transaction: Transaction) { + if (!view) return; + const newState = view.state.apply(transaction); + view.updateState(newState); + + // Re-layout on doc changes + if (transaction.docChanged) { + runLayoutPipeline(newState); + // Notify parent about document change + if (document.value) { + const updatedDoc = fromProseDoc(newState.doc, document.value); + onChange?.(updatedDoc); + } + } + }, + }); + + editorView.value = view; + isReady.value = true; + + // Initial layout + runLayoutPipeline(state); + } + + function destroyEditorView() { + if (editorView.value) { + editorView.value.destroy(); + editorView.value = null; + } + isReady.value = false; + } + + // ======================================================================== + // Document loading + // ======================================================================== + + async function loadBuffer(buffer: ArrayBuffer | Uint8Array | Blob | File) { + parseError.value = null; + isReady.value = false; + + try { + let arrayBuf: ArrayBuffer; + if (buffer instanceof Blob || buffer instanceof File) { + arrayBuf = await buffer.arrayBuffer(); + } else if (buffer instanceof Uint8Array) { + arrayBuf = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ) as ArrayBuffer; + } else { + arrayBuf = buffer; + } + + const doc = await parseDocx(arrayBuf); + document.value = doc; + + // Recreate PM view with new document + destroyEditorView(); + createEditorView(); + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + parseError.value = error.message; + onError?.(error); + } + } + + function loadDocument(doc: Document) { + parseError.value = null; + document.value = doc; + destroyEditorView(); + createEditorView(); + } + + // ======================================================================== + // Public API + // ======================================================================== + + async function save(): Promise { + if (!editorView.value || !document.value) return null; + + const { repackDocx, createDocx } = await import('@eigenpal/docx-core/docx/rezip'); + + const updatedDoc = fromProseDoc(editorView.value.state.doc, document.value); + let buffer: ArrayBuffer; + if (updatedDoc.originalBuffer) { + buffer = await repackDocx(updatedDoc); + } else { + buffer = await createDocx(updatedDoc); + } + return new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + } + + function focus() { + editorView.value?.focus(); + } + + function destroy() { + destroyEditorView(); + document.value = null; + } + + function getDocument(): Document | null { + return document.value; + } + + // ======================================================================== + // Lifecycle + // ======================================================================== + + onBeforeUnmount(() => { + destroy(); + }); + + return { + // State + editorView, + isReady, + parseError, + + // Actions + loadBuffer, + loadDocument, + save, + focus, + destroy, + getDocument, + }; +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 65bb80bc..d03b046d 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -4,7 +4,15 @@ // This package provides Vue 3 components wrapping @eigenpal/docx-core. // Contributions welcome! See the repository README for guidelines. -// renderAsync stub +// Components +export { default as DocxEditorVue } from './components/DocxEditorVue.vue'; +export { default as BasicToolbar } from './components/BasicToolbar.vue'; + +// Composables +export { useDocxEditor } from './composables/useDocxEditor'; +export type { UseDocxEditorOptions } from './composables/useDocxEditor'; + +// renderAsync export { renderAsync } from './renderAsync'; export type { VueRenderAsyncOptions } from './renderAsync'; diff --git a/packages/vue/src/renderAsync.ts b/packages/vue/src/renderAsync.ts index 02a0376e..92ec9417 100644 --- a/packages/vue/src/renderAsync.ts +++ b/packages/vue/src/renderAsync.ts @@ -1,13 +1,12 @@ /** - * Vue renderAsync — scaffold for community implementation. - * - * This is a placeholder that defines the expected API. - * A Vue contributor can implement this using `createApp().mount()`. + * Vue renderAsync — mounts a DocxEditorVue into a container element. */ +import { createApp, h, type App } from 'vue'; +import DocxEditorVue from './components/DocxEditorVue.vue'; import type { EditorHandle } from '@eigenpal/docx-core'; -/** Options for the Vue renderAsync (to be defined by implementor). */ +/** Options for the Vue renderAsync. */ export interface VueRenderAsyncOptions { readOnly?: boolean; showToolbar?: boolean; @@ -21,13 +20,59 @@ export interface VueRenderAsyncOptions { * @param options - Editor configuration * @returns A handle implementing the framework-agnostic EditorHandle interface */ -export function renderAsync( - _input: ArrayBuffer | Uint8Array | Blob | File, - _container: HTMLElement, - _options: VueRenderAsyncOptions = {} +export async function renderAsync( + input: ArrayBuffer | Uint8Array | Blob | File, + container: HTMLElement, + options: VueRenderAsyncOptions = {} ): Promise { - throw new Error( - '@eigenpal/docx-editor-vue renderAsync is not yet implemented. ' + - 'Community contributions welcome!' - ); + // Convert to ArrayBuffer upfront — loadBuffer also handles this, + // but we need a stable value for the prop. + let buffer: ArrayBuffer; + if (input instanceof Blob || input instanceof File) { + buffer = await input.arrayBuffer(); + } else if (input instanceof Uint8Array) { + buffer = input.buffer.slice( + input.byteOffset, + input.byteOffset + input.byteLength + ) as ArrayBuffer; + } else { + buffer = input; + } + + let editorRef: any = null; + + const app: App = createApp({ + setup() { + return () => + h(DocxEditorVue, { + documentBuffer: buffer, + showToolbar: options.showToolbar ?? true, + readOnly: options.readOnly ?? false, + ref: (el: any) => { + editorRef = el; + }, + }); + }, + }); + + app.mount(container); + + // Wait a tick for mount to complete + await new Promise((resolve) => setTimeout(resolve, 0)); + + return { + save: async () => { + return editorRef?.save() ?? null; + }, + getDocument: () => { + return editorRef?.getDocument() ?? null; + }, + focus: () => { + editorRef?.focus(); + }, + destroy: () => { + editorRef?.destroy(); + app.unmount(); + }, + }; } diff --git a/packages/vue/src/shims-vue.d.ts b/packages/vue/src/shims-vue.d.ts new file mode 100644 index 00000000..e16c3dc2 --- /dev/null +++ b/packages/vue/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent<{}, {}, any>; + export default component; +}