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