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 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
@@ -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 };