wordinweb 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/README.md +266 -4
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,19 +1,281 @@
1
1
  # WordInWeb
2
2
 
3
- React components for viewing and editing Word `.docx` files in the browser.
3
+ A Word/.docx viewer and editor for the web, embeddable as a single React component.
4
+
5
+ ```tsx
6
+ import { DocxView } from "wordinweb";
7
+
8
+ <DocxView source="/report.docx" />; // render-only viewer
9
+ ```
10
+
11
+ Editing is in an Alpha state and is strictly opt-in via the `editable` flag; the default is a pure viewer.
12
+
13
+ If you discover an edge case or perf issue create an issue and include the offending Word file.
14
+
15
+ Part of the broader roadmap is to migrate away from DOM rendering to just rendering on a canvas (or some hydrid thereof). Editing perf can suffer on really long documents.
16
+
17
+ ---
18
+
19
+ ## Install
4
20
 
5
21
  ```bash
6
22
  npm install wordinweb react react-dom
7
23
  ```
8
24
 
25
+ The package includes the `<DocxView />` component, optional `<DocxToolbar />`, parser, layout engine, and DOM renderer.
26
+
27
+ To match Word's glyph advances exactly, load the metric-compatible substitute fonts (Carlito ≈ Calibri, Caladea ≈ Cambria) in your app — see [Fonts](#fonts).
28
+
29
+ ## Quick start
30
+
31
+ ### View-only
32
+
9
33
  ```tsx
10
34
  import { DocxView } from "wordinweb";
11
35
 
12
36
  export function Preview() {
13
- return <DocxView source="/report.docx" style={{ height: "100vh" }} />;
37
+ return <DocxView source="/report.docx" zoom={1} style={{ height: "100vh" }} />;
38
+ }
39
+ ```
40
+
41
+ `source` accepts a URL string, `ArrayBuffer`, `Uint8Array`, `Blob`, or `File` — so a drag-and-drop or `<input type="file">` handler can pass its bytes straight through.
42
+
43
+ ### Editable (with the toolbar)
44
+
45
+ The editor exposes its commands through an imperative `api` handed to `onReady`. Pair it with the bundled `<DocxToolbar />` for a word-processor UI, or drive the `api` yourself.
46
+
47
+ ```tsx
48
+ import { useState } from "react";
49
+ import { DocxView, DocxToolbar, type DocxViewApi } from "wordinweb";
50
+
51
+ export function Editor() {
52
+ const [api, setApi] = useState<DocxViewApi | null>(null);
53
+
54
+ const download = (bytes: Uint8Array) => {
55
+ const blob = new Blob([bytes], {
56
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
57
+ });
58
+ const url = URL.createObjectURL(blob);
59
+ const a = document.createElement("a");
60
+ a.href = url;
61
+ a.download = "edited.docx";
62
+ a.click();
63
+ URL.revokeObjectURL(url);
64
+ };
65
+
66
+ return (
67
+ <div>
68
+ {api && <DocxToolbar api={api} onSave={download} />}
69
+ <DocxView
70
+ source="/report.docx"
71
+ editable
72
+ commentAuthor="Ada Lovelace"
73
+ onReady={setApi}
74
+ style={{ height: "80vh" }}
75
+ />
76
+ </div>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ## `DocxView` props
82
+
83
+ | Prop | Type | Default | What it does |
84
+ | --- | --- | --- | --- |
85
+ | `source` | `ArrayBuffer \| Uint8Array \| Blob \| string` | — | The document: raw bytes, a `File`/`Blob`, or a URL to fetch. |
86
+ | `zoom` | `number` | `1` | Zoom factor (`1` = 100%). Applied as a single `transform: scale()`; layout is unaffected. |
87
+ | `editable` | `boolean` | `false` | Enable editing (selection formatting, typing, save-back). Off = pure viewer. |
88
+ | `showComments` | `boolean` | `true` | Render review comments (range highlights + margin balloons). |
89
+ | `revisions` | `"final" \| "markup"` | `"final"` | Tracked-changes display: clean final text, or insertions underlined / deletions struck. |
90
+ | `commentAuthor` | `string` | `"You"` | Name stamped on comments, replies, and suggestions. |
91
+ | `onLoad` | `(info: { pageCount; document }) => void` | — | Fires once the document is parsed and laid out. |
92
+ | `onReady` | `(api: DocxViewApi) => void` | — | Fires when editing is wired up; the `api` is valid only while mounted. Only called when `editable`. |
93
+ | `onMissingFonts` | `(missing: MissingFont[]) => void` | — | Fires after render with any requested faces the browser can't render (silently substituted; layout may differ from Word). Empty array = all good. |
94
+ | `onError` | `(error: Error) => void` | — | Fires if parsing or rendering throws. |
95
+ | `className` | `string` | — | Class on the scroll container. |
96
+ | `style` | `React.CSSProperties` | — | Inline styles on the scroll container (set `height` here). |
97
+
98
+ ## `DocxViewApi`
99
+
100
+ The object passed to `onReady`. Every command operates on the current selection or caret and re-lays out in place. Grouped by area:
101
+
102
+ **Character & paragraph formatting**
103
+ - `applyFormat(patch)` — bold, italic, underline, strike, `fontSizePt`, `fontFamily`, `color`, `highlight`, `verticalAlign` (super/subscript), or `clear`, over any selection (including partial runs).
104
+ - `getSelectionFormat()` — effective formatting of the selection (drives toolbar state).
105
+ - `clearFormatting()`, `changeCase("upper" | "lower" | "title")`.
106
+ - `setAlignment("left" | "center" | "right" | "justify")`.
107
+ - `setParagraphStyle(styleId | null)`, `listParagraphStyles()`, `getParagraphStyleId()`.
108
+ - `toggleList("bullet" | "number")`, `getListType()`.
109
+ - `adjustIndent(1 | -1)`, `setParagraphSpacing({ lineMultiple?, beforePt?, afterPt? })`.
110
+ - `setLink(url | null)`, `getLinkAt()`.
111
+
112
+ **Page layout**
113
+ - `setPageLayout(patch, scope?)` — margins, page size, orientation, columns, page borders. `scope` = `"document"` (all sections) or `"section"` (the caret's).
114
+ - `insertBreak("page" | "column" | "sectionNextPage" | "sectionContinuous")`.
115
+ - `setLineNumbering(patch, scope?)`, `getLineNumbering()`.
116
+
117
+ **Tables**
118
+ - `insertTable(rows, cols)`.
119
+ - `tableOp(op)` — insert/delete row/column, merge/split cells, cell vertical align, cell shading, delete table.
120
+
121
+ **Images**
122
+ - `insertImage(file)` — inserts a `Blob`/`File` inline at the caret, clamped to the column width (TIFF/WMF/EMF are decoded to something the browser can paint).
123
+
124
+ **Comments, footnotes & fields**
125
+ - `addComment(text)` — comment on the selection (`false` if nothing is selected).
126
+ - `addFootnote(text)` — footnote at the caret.
127
+ - `insertPageNumber("page" | "pageOfTotal")` — dynamic `PAGE` / `Page X of Y` field.
128
+
129
+ **Suggesting mode (tracked changes)**
130
+ - `setSuggesting(on, author?)` — when on, edits record as OOXML `w:ins`/`w:del` instead of mutating text directly, and the view switches to markup.
131
+ - `isSuggesting()`, `acceptRevisionAtCaret()`, `rejectRevisionAtCaret()`.
132
+
133
+ **Find & replace**
134
+ - `find(query, { matchCase? })` → match count (selects the first).
135
+ - `findStep(1 | -1)` → 1-based index of the newly selected match.
136
+ - `replaceCurrent(replacement)` → remaining matches. `replaceAll(query, replacement)` → count replaced.
137
+
138
+ **History, save & print**
139
+ - `undo()`, `redo()`, `canUndo()`, `canRedo()`.
140
+ - `save()` → edited `.docx` bytes (`Uint8Array`). Everything the model doesn't touch round-trips byte-for-byte.
141
+ - `print()` — browser print dialog / save-as-PDF of the rendered pages.
142
+ - `pageCount()`, `closeHeaderFooter()`, and `document` (the parsed `DocxDocument`).
143
+
144
+ ### `DocxToolbar`
145
+
146
+ A ready-made formatting toolbar for an editable `DocxView`. Ribbon-style tabs (Home / Insert / Layout) wired to the `api`.
147
+
148
+ | Prop | Type | What it does |
149
+ | --- | --- | --- |
150
+ | `api` | `DocxViewApi \| null` | The api from `onReady`. |
151
+ | `onSave` | `(bytes: Uint8Array) => void` | Adds a Download button; receives `api.save()` bytes. |
152
+ | `features` | `Partial<Record<ToolbarFeature, boolean>>` | Per-group toggles; every group defaults on. Set one `false` to hide it (e.g. `{ table: false, image: false }`). |
153
+ | `className` | `string` | Class on the toolbar root — handy as a scope for CSS-variable overrides. |
154
+ | `style` | `React.CSSProperties` | Inline overrides merged onto the toolbar root. |
155
+
156
+ ## Theming
157
+
158
+ Every color the chrome paints — the toolbar, comment cards, selection, caret, page shadow — routes through a CSS custom property with the stock value as its fallback. The **default look is unchanged**; set any variable on an ancestor element (custom properties inherit) to retheme. Wrap the toolbar and viewer in one element and set the variables there, or set them on `:root` to theme globally.
159
+
160
+ | Variable | Default | Controls |
161
+ | --- | --- | --- |
162
+ | `--dxw-toolbar-bg` | `#f9fbfd` | Toolbar bar background |
163
+ | `--dxw-toolbar-fg` | `#3c4043` | Toolbar text & icons (icons use `currentColor`) |
164
+ | `--dxw-toolbar-border` | `#dadce0` | Toolbar borders & separators |
165
+ | `--dxw-toolbar-muted` | `#5f6368` | Secondary/label text |
166
+ | `--dxw-accent` | `#1a73e8` | Primary accent (active tab, pills, focus rings) |
167
+ | `--dxw-accent-fg` | `#fff` | Text/icon on an accent fill |
168
+ | `--dxw-btn-active-bg` | `#dfe7f5` | Toggled button background |
169
+ | `--dxw-btn-hover-bg` | `#f1f3f4` | Button hover background |
170
+ | `--dxw-tab-active-bg` | `#e8f0fe` | Active ribbon-tab background |
171
+ | `--dxw-popover-bg` | `#fff` | Dropdown / popover surface |
172
+ | `--dxw-popover-shadow` | `0 4px 16px rgba(0,0,0,.15)` | Popover shadow |
173
+ | `--dxw-canvas-bg` | `#e8eaed` | Scroll area behind the pages |
174
+ | `--dxw-page-bg` | `#ffffff` | Page paper |
175
+ | `--dxw-page-shadow` | `0 1px 3px …, 0 4px 14px …` | Page drop shadow |
176
+ | `--dxw-caret` | `#1a1a1a` | Text caret |
177
+ | `--dxw-selection` | `rgba(26,115,232,.28)` | Text-selection highlight |
178
+ | `--dxw-comment-hl` | `rgba(255,200,90,.38)` | Commented-range highlight |
179
+ | `--dxw-comment-hl-active` | `rgba(255,170,0,.55)` | Active commented-range highlight |
180
+ | `--dxw-comment-bg` | `#fff` | Comment card surface |
181
+ | `--dxw-comment-fg` | `#3c4043` | Comment card text |
182
+ | `--dxw-comment-border` | `#e0e0e0` | Comment card border |
183
+
184
+ ### Dark toolbar example
185
+
186
+ ```tsx
187
+ <div
188
+ className="dxw-dark"
189
+ style={{
190
+ ["--dxw-toolbar-bg" as string]: "#1f2937",
191
+ ["--dxw-toolbar-fg" as string]: "#e5e7eb",
192
+ ["--dxw-toolbar-border" as string]: "#374151",
193
+ ["--dxw-toolbar-muted" as string]: "#9ca3af",
194
+ ["--dxw-accent" as string]: "#60a5fa",
195
+ ["--dxw-btn-active-bg" as string]: "#374151",
196
+ ["--dxw-btn-hover-bg" as string]: "#2b3644",
197
+ ["--dxw-tab-active-bg" as string]: "#334155",
198
+ ["--dxw-popover-bg" as string]: "#1f2937",
199
+ ["--dxw-canvas-bg" as string]: "#111827",
200
+ }}
201
+ >
202
+ {api && <DocxToolbar api={api} onSave={download} />}
203
+ <DocxView source="/report.docx" editable onReady={setApi} style={{ height: "80vh" }} />
204
+ </div>
205
+ ```
206
+
207
+ Or in a stylesheet:
208
+
209
+ ```css
210
+ .dxw-dark { --dxw-toolbar-bg: #1f2937; --dxw-toolbar-fg: #e5e7eb; /* … */ }
211
+ ```
212
+
213
+ ## How it works
214
+
215
+ WordInWeb never converts the document to flowing HTML. It parses the OOXML into a typed model, runs a layout engine that breaks lines with real canvas metrics and paginates like Word, and renders each primitive as one absolutely-positioned element, so the browser does zero reflow. Editing mutates the retained XML tree and re-serializes only the parts it models, leaving everything else byte-for-byte intact.
216
+
217
+ See [`BLOG.md`](BLOG.md) for the pipeline and the parity work.
218
+
219
+ ## Fonts
220
+
221
+ Text is measured on a canvas before layout, so line breaks depend on the real font metrics. For Word parity, register the bundled OFL substitutes in your app entry:
222
+
223
+ ```ts
224
+ import "@fontsource/carlito/400.css"; // Calibri metrics
225
+ import "@fontsource/carlito/400-italic.css";
226
+ import "@fontsource/carlito/700.css";
227
+ import "@fontsource/carlito/700-italic.css";
228
+ import "@fontsource/caladea/400.css"; // Cambria metrics
229
+ import "@fontsource/caladea/700.css";
230
+ ```
231
+
232
+ > **The Microsoft fonts in the separate parity demo are demo-only assets.** They
233
+ > are included to reproduce this project's experimental parity results, but
234
+ > they are not part of the WordInWeb library and should not be copied into
235
+ > another application.
236
+ > Applications using WordInWeb must source and license their own fonts. Without
237
+ > the same fonts, the viewer falls back to substitutes and glyphs or line breaks
238
+ > may differ from Word.
239
+
240
+ For your own application, source your own fonts.
241
+
242
+ ```css
243
+ @font-face {
244
+ font-family: "Calibri";
245
+ src: local("Calibri"), url("/fonts/Calibri.woff2") format("woff2");
246
+ font-weight: 400;
247
+ font-style: normal;
248
+ font-display: swap;
14
249
  }
15
250
  ```
16
251
 
17
- Documentation and source: https://github.com/theRealestAEP/wordinweb
252
+ Add a separate `@font-face` rule for every weight and style you use.
253
+
254
+ When the browser can't render a requested face, `onMissingFonts` reports it so you can warn the user that the on-screen layout may drift from Word.
255
+
256
+ ## Package structure
257
+
258
+ The repository keeps the parser and layout engine in an internal `core` workspace for development. The published `wordinweb` package bundles that workspace with the React components, so applications install one package.
259
+
260
+ The demo, browser tests, fixture corpus, and Word parity references live in the
261
+ separate [wordinweb-parity](https://github.com/theRealestAEP/wordinweb-parity)
262
+ repository. That repository links this one as a Git submodule.
263
+
264
+ ## Development
265
+
266
+ ```bash
267
+ npm install
268
+ npm test # core unit tests (parser + layout, deterministic measurer)
269
+ npm run build # build the public package
270
+ ```
271
+
272
+ ## Rendering parity
273
+
274
+ Fidelity is measured against desktop Microsoft Word. Word exports each fixture
275
+ to PDF, WordInWeb renders the same file in the browser, and both are rasterized
276
+ and compared page-by-page. The test corpus and instructions are in
277
+ [wordinweb-parity](https://github.com/theRealestAEP/wordinweb-parity).
278
+
279
+ ## License
18
280
 
19
- Licensed for noncommercial use under PolyForm Noncommercial 1.0.0.
281
+ PolyForm Noncommercial 1.0.0 — free for noncommercial use; commercial use requires permission. See LICENSE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wordinweb",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "React component for high-fidelity DOCX viewing",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "repository": {