simple-super-doc 0.11.3
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 +151 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +2221 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rafael-T-Santos
|
|
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,151 @@
|
|
|
1
|
+
# simple-super-doc
|
|
2
|
+
|
|
3
|
+
Browser-only TypeScript library that parses a `.docx` file into a typed
|
|
4
|
+
intermediate representation (IR) and renders it to HTML. Built for document
|
|
5
|
+
editors and viewers that need a faithful, inspectable model of the document.
|
|
6
|
+
|
|
7
|
+
- **Typed IR** — `parse()` returns a `DocxDocument` of `Block`s (paragraphs,
|
|
8
|
+
tables) with computed styles, lists, images, and page size. Inspect or
|
|
9
|
+
transform it before rendering.
|
|
10
|
+
- **HTML rendering** — `render()` paints the IR into a container element.
|
|
11
|
+
- **No DOM at parse time** — parsing is pure; only `render()` touches the DOM.
|
|
12
|
+
- **Security** — text is written with `textContent`, never `innerHTML`; hyperlink
|
|
13
|
+
hrefs and image sources are scheme-sanitized (only `http(s)`/`mailto`/`tel`/
|
|
14
|
+
relative links and `data:image`/`http(s)` images), so a malicious `.docx` can't
|
|
15
|
+
inject a `javascript:` or other unsafe URL.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install simple-super-doc
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { parse, render } from 'simple-super-doc'
|
|
27
|
+
|
|
28
|
+
const buf = await file.arrayBuffer() // a .docx File/Blob
|
|
29
|
+
const doc = await parse(buf) // typed DocxDocument IR
|
|
30
|
+
render(doc, document.getElementById('view')!)
|
|
31
|
+
|
|
32
|
+
// Optional: show tracked changes (deletions struck through, insertions
|
|
33
|
+
// underlined) instead of the accepted/final view.
|
|
34
|
+
render(doc, view, { showRevisions: true })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`parse` throws `DocxParseError` (`code: 'INVALID_ZIP' | 'MISSING_ENTRY'`) for
|
|
38
|
+
non-docx or malformed input.
|
|
39
|
+
|
|
40
|
+
## What it covers
|
|
41
|
+
|
|
42
|
+
- **Text & styles** — bold/italic/underline/strikethrough, super/subscript,
|
|
43
|
+
font size/family, color, highlight and shading, paragraph alignment
|
|
44
|
+
(including OOXML justified `both`), spacing, indentation and borders, and the
|
|
45
|
+
full style cascade (docDefaults → named style → direct formatting).
|
|
46
|
+
- **Structure** — numbered and bulleted lists (nested), tables with
|
|
47
|
+
`gridSpan`/`vMerge`, column widths, cell margins and cell borders (resolved by
|
|
48
|
+
cascade: table style → `tblBorders` → `tcBorders`, so borders that come only
|
|
49
|
+
from a table style like `TableGrid` are drawn), and block/run ordering
|
|
50
|
+
recovered from the raw XML (paragraphs, tables, mid-paragraph hyperlinks and
|
|
51
|
+
tracked changes keep their real sequence — in the body **and inside cells**).
|
|
52
|
+
- **Images** — inline and anchored DrawingML (`a:blip`) and legacy VML
|
|
53
|
+
(`w:pict`/`v:imagedata`), as base64 data URLs; external/linked images
|
|
54
|
+
(`r:link`) render from their URL. (EMF/WMF metafiles are skipped — browsers
|
|
55
|
+
can't display them.)
|
|
56
|
+
- **Content controls & wrappers** — `w:sdt` (structured document tags / form
|
|
57
|
+
controls), `w:smartTag` and `w:customXml` are transparent: their content
|
|
58
|
+
(block or inline, possibly nested) renders in place instead of being dropped.
|
|
59
|
+
- **Headers & footers** — default `headerReference`/`footerReference`, rendered
|
|
60
|
+
in the page margins on every page; distinct per section, with a section that
|
|
61
|
+
declares none inheriting the previous section's (OOXML semantics); distinct
|
|
62
|
+
first-page (`w:titlePg`) and even-page (`w:evenAndOddHeaders`) variants.
|
|
63
|
+
- **Page breaks** — `w:pageBreakBefore` and explicit `<w:br w:type="page"/>`.
|
|
64
|
+
- **Tab stops** — right/center/decimal stops with dot/hyphen/underscore leaders
|
|
65
|
+
(table-of-contents rows render as `Title …… 12`).
|
|
66
|
+
- **Fields** — `PAGE` and `NUMPAGES` resolve live (robust to `\* MERGEFORMAT`
|
|
67
|
+
switches); other fields (DATE, REF, PAGEREF, TOC, HYPERLINK, …) render their
|
|
68
|
+
cached result. Both the complex `fldChar` form (whether split across runs or
|
|
69
|
+
packed into a single run, as Google Docs exports) and the compact `w:fldSimple`
|
|
70
|
+
form are supported.
|
|
71
|
+
- **Footnotes & endnotes** — footnotes at the bottom of the referencing page;
|
|
72
|
+
endnotes on a final page.
|
|
73
|
+
- **Tracked changes** — deletions, insertions, and moves (`w:moveFrom`/
|
|
74
|
+
`w:moveTo`), shown or hidden via the `showRevisions` render option.
|
|
75
|
+
- **Right-to-left** — `w:bidi` paragraphs and `w:rtl` runs (Arabic/Hebrew) render
|
|
76
|
+
with `dir="rtl"`; Unicode text (incl. CJK) is preserved as-is.
|
|
77
|
+
- **Equations** — OMML (`m:oMath`) renders as its linear text (inline, display
|
|
78
|
+
and in table cells); 2D layout is out of scope (see below).
|
|
79
|
+
- **Multiple sections** — per-section page size and orientation (e.g. a
|
|
80
|
+
landscape table page between portrait pages), per-section headers/footers,
|
|
81
|
+
with continuous page numbering. Each section is routed to the plain or
|
|
82
|
+
full-page-background path by its own content, so template documents may mix
|
|
83
|
+
page sizes across sections.
|
|
84
|
+
|
|
85
|
+
## Page-aware rendering
|
|
86
|
+
|
|
87
|
+
Some templates paginate themselves with full-page background images (a cover
|
|
88
|
+
slide, framed body pages, a closing slide) but carry **no explicit page
|
|
89
|
+
breaks** — no `<w:br w:type="page"/>` and no `<w:lastRenderedPageBreak/>`.
|
|
90
|
+
Word/LibreOffice decide the pages purely in their layout engine.
|
|
91
|
+
|
|
92
|
+
When `render()` detects a full-page background (a `behindDoc=1` anchor), it
|
|
93
|
+
reconstructs the pages with a two-pass approach and a set of heuristics. These
|
|
94
|
+
live in [`src/renderer/layout.ts`](src/renderer/layout.ts) as pure, unit-tested
|
|
95
|
+
functions ([`test/layout-heuristics.test.ts`](test/layout-heuristics.test.ts)):
|
|
96
|
+
|
|
97
|
+
- **Two-pass measurement** — each block is measured in a hidden container
|
|
98
|
+
(Pass 1), then content is distributed into page-sized boxes (Pass 2). This
|
|
99
|
+
needs a real layout engine, so it runs in the browser, not jsdom.
|
|
100
|
+
- **Empty paragraphs are spacers** — they collapse to 0px in a browser but
|
|
101
|
+
occupy a line in Word, so empty paragraphs get a min line height
|
|
102
|
+
(`EMPTY_LINE_EM`). Their vertical rhythm positions things like a cover's
|
|
103
|
+
customer-name field.
|
|
104
|
+
- **Section headings force a new page** — a heading is large text
|
|
105
|
+
(`fontSize >= 24`) or a wide-short "text-as-image" banner; a small icon that
|
|
106
|
+
precedes a text heading travels with it. Each framed section starts a page.
|
|
107
|
+
- **Watermarks overlay, not flow** — large floating images (decorative
|
|
108
|
+
frames/watermarks) are absolutely positioned behind the text and contribute
|
|
109
|
+
zero pagination height, instead of pushing content onto extra pages.
|
|
110
|
+
- **Background regions by image order** — floating backgrounds are positioned
|
|
111
|
+
by absolute page coordinates, so the *order* of distinct background images
|
|
112
|
+
drives which page each covers (cover → page 0, body frame → pages 1+), not
|
|
113
|
+
their XML position.
|
|
114
|
+
- **Full-bleed slides** — a page whose only content is a near-full-width image
|
|
115
|
+
is drawn edge-to-edge; blank pages are dropped.
|
|
116
|
+
|
|
117
|
+
These thresholds are tuned for proposal/letter-style templates. A document
|
|
118
|
+
without a full-page background skips all of this and renders as a normal flow.
|
|
119
|
+
|
|
120
|
+
## Limitations & non-goals
|
|
121
|
+
|
|
122
|
+
This is a faithful but *simple* HTML renderer, not a Word layout engine. The
|
|
123
|
+
following are intentionally out of scope:
|
|
124
|
+
|
|
125
|
+
- **Floating layout** — text wrapping around floating images, multi-column
|
|
126
|
+
layouts, shapes, charts, and SmartArt are not laid out. (Inline images and
|
|
127
|
+
tables are supported.) Text inside a text box (`w:txbxContent`, DrawingML or
|
|
128
|
+
VML) is recovered into the flow so it is never lost, but the box is not
|
|
129
|
+
floated or positioned.
|
|
130
|
+
- **2D math layout** — OMML equations (`m:oMath`) are recovered as their linear
|
|
131
|
+
text (e.g. `A=πr2`), inline and in order, so the content is never lost, but
|
|
132
|
+
fractions, superscripts and radicals are not laid out two-dimensionally.
|
|
133
|
+
- **Pixel-exact pagination** — without Word's line-breaking and layout engine,
|
|
134
|
+
page breaks are reconstructed by two-pass DOM measurement and heuristics.
|
|
135
|
+
Pagination is close but not guaranteed to match Word/LibreOffice line for
|
|
136
|
+
line. For a byte-faithful page image, convert the `.docx` to PDF.
|
|
137
|
+
- **Comments** — review comments are parsed away (treated as noise); only
|
|
138
|
+
tracked-change insertions/deletions are surfaced (via `showRevisions`).
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm run typecheck # tsc --noEmit
|
|
144
|
+
npm test # vitest
|
|
145
|
+
npm run build # esm + d.ts into dist/
|
|
146
|
+
npm run build:demo # IIFE bundle into demo/ for the drag-and-drop demo
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
type DocxDocument = {
|
|
2
|
+
blocks: Block[];
|
|
3
|
+
pageSize?: {
|
|
4
|
+
widthPx: number;
|
|
5
|
+
heightPx: number;
|
|
6
|
+
marginPx: {
|
|
7
|
+
top: number;
|
|
8
|
+
right: number;
|
|
9
|
+
bottom: number;
|
|
10
|
+
left: number;
|
|
11
|
+
};
|
|
12
|
+
footerPx?: number;
|
|
13
|
+
headerPx?: number;
|
|
14
|
+
};
|
|
15
|
+
footnotes?: NoteEntry[];
|
|
16
|
+
endnotes?: NoteEntry[];
|
|
17
|
+
footer?: Block[];
|
|
18
|
+
header?: Block[];
|
|
19
|
+
headerFirst?: Block[];
|
|
20
|
+
headerEven?: Block[];
|
|
21
|
+
footerFirst?: Block[];
|
|
22
|
+
footerEven?: Block[];
|
|
23
|
+
titlePg?: boolean;
|
|
24
|
+
evenAndOddHeaders?: boolean;
|
|
25
|
+
sections?: Section[];
|
|
26
|
+
};
|
|
27
|
+
type PageSize = NonNullable<DocxDocument['pageSize']>;
|
|
28
|
+
type Section = {
|
|
29
|
+
blocks: Block[];
|
|
30
|
+
pageSize: PageSize;
|
|
31
|
+
header?: Block[];
|
|
32
|
+
footer?: Block[];
|
|
33
|
+
};
|
|
34
|
+
type NoteEntry = {
|
|
35
|
+
number: number;
|
|
36
|
+
blocks: Block[];
|
|
37
|
+
};
|
|
38
|
+
type Block = ParagraphBlock | TableBlock;
|
|
39
|
+
type ParagraphBlock = {
|
|
40
|
+
type: 'paragraph';
|
|
41
|
+
style: ComputedStyle;
|
|
42
|
+
runs: Run[];
|
|
43
|
+
list?: ListRef;
|
|
44
|
+
pageBreakBefore?: boolean;
|
|
45
|
+
sectionPageSize?: PageSize;
|
|
46
|
+
sectionRefs?: {
|
|
47
|
+
headerRId?: string;
|
|
48
|
+
footerRId?: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
type Run = TextRun | ImageRun;
|
|
52
|
+
type TextRun = {
|
|
53
|
+
type: 'run';
|
|
54
|
+
text: string;
|
|
55
|
+
style: ComputedStyle;
|
|
56
|
+
href?: string;
|
|
57
|
+
noteRef?: {
|
|
58
|
+
type: 'footnote' | 'endnote';
|
|
59
|
+
number: number;
|
|
60
|
+
};
|
|
61
|
+
pageNumber?: boolean;
|
|
62
|
+
totalPages?: boolean;
|
|
63
|
+
lineBreak?: boolean;
|
|
64
|
+
pageBreak?: boolean;
|
|
65
|
+
tabs?: number;
|
|
66
|
+
deleted?: boolean;
|
|
67
|
+
inserted?: boolean;
|
|
68
|
+
};
|
|
69
|
+
type RenderOptions = {
|
|
70
|
+
showRevisions?: boolean;
|
|
71
|
+
};
|
|
72
|
+
type ImageRun = {
|
|
73
|
+
type: 'image';
|
|
74
|
+
src: string;
|
|
75
|
+
widthPx: number;
|
|
76
|
+
heightPx: number;
|
|
77
|
+
isPageBackground?: boolean;
|
|
78
|
+
href?: string;
|
|
79
|
+
};
|
|
80
|
+
type TableBlock = {
|
|
81
|
+
type: 'table';
|
|
82
|
+
rows: TableRow[];
|
|
83
|
+
columnWidths?: number[];
|
|
84
|
+
cellPadding?: {
|
|
85
|
+
top: number;
|
|
86
|
+
right: number;
|
|
87
|
+
bottom: number;
|
|
88
|
+
left: number;
|
|
89
|
+
};
|
|
90
|
+
align?: 'left' | 'center' | 'right';
|
|
91
|
+
};
|
|
92
|
+
type TableRow = {
|
|
93
|
+
cells: TableCell[];
|
|
94
|
+
heightPx?: number;
|
|
95
|
+
heightExact?: boolean;
|
|
96
|
+
};
|
|
97
|
+
type TableCell = {
|
|
98
|
+
rowSpan: number;
|
|
99
|
+
colSpan: number;
|
|
100
|
+
blocks: Block[];
|
|
101
|
+
backgroundColor?: string;
|
|
102
|
+
border?: CellBorders;
|
|
103
|
+
};
|
|
104
|
+
type CellBorders = {
|
|
105
|
+
top?: string;
|
|
106
|
+
right?: string;
|
|
107
|
+
bottom?: string;
|
|
108
|
+
left?: string;
|
|
109
|
+
};
|
|
110
|
+
type ListRef = {
|
|
111
|
+
numId: string;
|
|
112
|
+
ilvl: number;
|
|
113
|
+
ordered: boolean;
|
|
114
|
+
start: number;
|
|
115
|
+
format: string;
|
|
116
|
+
};
|
|
117
|
+
type ComputedStyle = {
|
|
118
|
+
bold?: boolean;
|
|
119
|
+
italic?: boolean;
|
|
120
|
+
underline?: boolean;
|
|
121
|
+
strike?: boolean;
|
|
122
|
+
vertAlign?: 'super' | 'sub';
|
|
123
|
+
fontSize?: number;
|
|
124
|
+
fontFamily?: string;
|
|
125
|
+
color?: string;
|
|
126
|
+
alignment?: 'left' | 'center' | 'right' | 'justify';
|
|
127
|
+
rtl?: boolean;
|
|
128
|
+
backgroundColor?: string;
|
|
129
|
+
highlight?: string;
|
|
130
|
+
spaceBefore?: number;
|
|
131
|
+
spaceAfter?: number;
|
|
132
|
+
lineHeight?: number;
|
|
133
|
+
lineHeightPx?: number;
|
|
134
|
+
indentLeft?: number;
|
|
135
|
+
indentRight?: number;
|
|
136
|
+
indentFirstLine?: number;
|
|
137
|
+
indentHanging?: number;
|
|
138
|
+
borderTop?: string;
|
|
139
|
+
borderBottom?: string;
|
|
140
|
+
borderLeft?: string;
|
|
141
|
+
borderRight?: string;
|
|
142
|
+
tabStops?: TabStop[];
|
|
143
|
+
};
|
|
144
|
+
type TabStop = {
|
|
145
|
+
posPx: number;
|
|
146
|
+
val: 'left' | 'right' | 'center' | 'decimal' | 'bar';
|
|
147
|
+
leader: 'none' | 'dot' | 'hyphen' | 'underscore';
|
|
148
|
+
};
|
|
149
|
+
declare class DocxParseError extends Error {
|
|
150
|
+
code: 'MISSING_ENTRY' | 'INVALID_ZIP';
|
|
151
|
+
constructor(message: string, code: 'MISSING_ENTRY' | 'INVALID_ZIP');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
declare function parse(buffer: ArrayBuffer): Promise<DocxDocument>;
|
|
155
|
+
declare function render(doc: DocxDocument, container: HTMLElement, options?: RenderOptions): void;
|
|
156
|
+
|
|
157
|
+
export { type Block, type ComputedStyle, type DocxDocument, DocxParseError, type ImageRun, type ListRef, type NoteEntry, type PageSize, type ParagraphBlock, type RenderOptions, type Run, type Section, type TabStop, type TableBlock, type TableCell, type TableRow, type TextRun, parse, render };
|