simple-super-doc 0.11.3 → 0.11.5
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 +14 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +94 -34
- package/package.json +19 -4
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ Browser-only TypeScript library that parses a `.docx` file into a typed
|
|
|
4
4
|
intermediate representation (IR) and renders it to HTML. Built for document
|
|
5
5
|
editors and viewers that need a faithful, inspectable model of the document.
|
|
6
6
|
|
|
7
|
+
**▶️ [Try it live](https://rafael-t-santos.github.io/simple-super-doc/)** — drag
|
|
8
|
+
and drop any `.docx` in your browser to see it rendered. Nothing is uploaded;
|
|
9
|
+
parsing and rendering happen entirely on your machine.
|
|
10
|
+
|
|
7
11
|
- **Typed IR** — `parse()` returns a `DocxDocument` of `Block`s (paragraphs,
|
|
8
12
|
tables) with computed styles, lists, images, and page size. Inspect or
|
|
9
13
|
transform it before rendering.
|
|
@@ -140,12 +144,18 @@ following are intentionally out of scope:
|
|
|
140
144
|
## Development
|
|
141
145
|
|
|
142
146
|
```bash
|
|
143
|
-
npm run typecheck
|
|
144
|
-
npm test
|
|
145
|
-
npm run
|
|
146
|
-
npm run build
|
|
147
|
+
npm run typecheck # tsc --noEmit
|
|
148
|
+
npm test # vitest (node) — parser + pure layout heuristics
|
|
149
|
+
npm run test:browser # Playwright (headless Chromium) — page-aware rendering
|
|
150
|
+
npm run build # esm + d.ts into dist/
|
|
151
|
+
npm run build:demo # IIFE bundle into demo/ for the drag-and-drop demo
|
|
147
152
|
```
|
|
148
153
|
|
|
154
|
+
The two-pass, page-aware pagination (table splitting, per-page footnotes) needs
|
|
155
|
+
a real layout engine, so it's covered by browser tests in
|
|
156
|
+
[`test/browser/`](test/browser/) rather than the node suite. Install the browser
|
|
157
|
+
once with `npx playwright-core install chromium`.
|
|
158
|
+
|
|
149
159
|
## License
|
|
150
160
|
|
|
151
161
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ type ImageRun = {
|
|
|
76
76
|
heightPx: number;
|
|
77
77
|
isPageBackground?: boolean;
|
|
78
78
|
href?: string;
|
|
79
|
+
anchorXPx?: number;
|
|
80
|
+
anchorYPx?: number;
|
|
79
81
|
};
|
|
80
82
|
type TableBlock = {
|
|
81
83
|
type: 'table';
|
|
@@ -113,6 +115,7 @@ type ListRef = {
|
|
|
113
115
|
ordered: boolean;
|
|
114
116
|
start: number;
|
|
115
117
|
format: string;
|
|
118
|
+
bulletText?: string;
|
|
116
119
|
};
|
|
117
120
|
type ComputedStyle = {
|
|
118
121
|
bold?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -290,9 +290,11 @@ function parseNumbering(xml) {
|
|
|
290
290
|
const ilvl = parseInt(String(lvl.ilvl), 10);
|
|
291
291
|
const fmtNode = lvl.numFmt;
|
|
292
292
|
const startNode = lvl.start;
|
|
293
|
+
const textNode = lvl.lvlText;
|
|
293
294
|
const format = fmtNode?.val ?? "bullet";
|
|
294
295
|
const start = parseInt(startNode?.val ?? "1", 10);
|
|
295
|
-
|
|
296
|
+
const text = textNode?.val;
|
|
297
|
+
abstractNumMap[id][ilvl] = { format, start: isNaN(start) ? 1 : start, ...text !== void 0 ? { text } : {} };
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
const numMap = {};
|
|
@@ -465,7 +467,14 @@ function resolveListRef(pPr, ctx) {
|
|
|
465
467
|
if (!levelInfo) return void 0;
|
|
466
468
|
const start = numEntry.startOverride ?? levelInfo.start;
|
|
467
469
|
const ordered = levelInfo.format !== "bullet" && levelInfo.format !== "none";
|
|
468
|
-
return {
|
|
470
|
+
return {
|
|
471
|
+
numId,
|
|
472
|
+
ilvl,
|
|
473
|
+
ordered,
|
|
474
|
+
start,
|
|
475
|
+
format: levelInfo.format,
|
|
476
|
+
...levelInfo.text !== void 0 ? { bulletText: levelInfo.text } : {}
|
|
477
|
+
};
|
|
469
478
|
}
|
|
470
479
|
async function parseRun(r, paraStyle, ctx, href) {
|
|
471
480
|
const rPr = r.rPr;
|
|
@@ -513,13 +522,22 @@ async function parseRun(r, paraStyle, ctx, href) {
|
|
|
513
522
|
if (!rId) return null;
|
|
514
523
|
const resolved = await resolveImage(rId, ctx.relationshipMap, ctx.zip);
|
|
515
524
|
if (!resolved) return null;
|
|
525
|
+
const offsetPx = (pos) => {
|
|
526
|
+
const off = pos?.posOffset;
|
|
527
|
+
const n = off != null ? parseInt(String(off["#text"] ?? off), 10) : NaN;
|
|
528
|
+
return Number.isNaN(n) ? void 0 : Math.round(n / 9525);
|
|
529
|
+
};
|
|
530
|
+
const anchorXPx = anchor ? offsetPx(anchor.positionH) : void 0;
|
|
531
|
+
const anchorYPx = anchor ? offsetPx(anchor.positionV) : void 0;
|
|
516
532
|
const img = {
|
|
517
533
|
type: "image",
|
|
518
534
|
src: resolved.src,
|
|
519
535
|
widthPx: Math.round(cx / 9525),
|
|
520
536
|
heightPx: Math.round(cy / 9525),
|
|
521
537
|
...isPageBackground ? { isPageBackground: true } : {},
|
|
522
|
-
...href ? { href } : {}
|
|
538
|
+
...href ? { href } : {},
|
|
539
|
+
...anchorXPx !== void 0 ? { anchorXPx } : {},
|
|
540
|
+
...anchorYPx !== void 0 ? { anchorYPx } : {}
|
|
523
541
|
};
|
|
524
542
|
return img;
|
|
525
543
|
}
|
|
@@ -540,27 +558,32 @@ async function parseRun(r, paraStyle, ctx, href) {
|
|
|
540
558
|
}
|
|
541
559
|
return null;
|
|
542
560
|
}
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
561
|
+
const collectText = (node) => {
|
|
562
|
+
if (node == null) return [];
|
|
563
|
+
if (Array.isArray(node)) return node.flatMap(collectText);
|
|
564
|
+
if (typeof node === "string") return [node];
|
|
565
|
+
if (typeof node === "object") {
|
|
566
|
+
const t = node;
|
|
567
|
+
return [String(t["#text"] ?? t._ ?? "")];
|
|
568
|
+
}
|
|
569
|
+
return [String(node)];
|
|
570
|
+
};
|
|
571
|
+
const segments = collectText(r.t ?? r.delText);
|
|
553
572
|
const tabNode = r.tab;
|
|
554
|
-
const
|
|
555
|
-
const
|
|
573
|
+
const tabCount = Array.isArray(tabNode) ? tabNode.length : "tab" in r ? 1 : 0;
|
|
574
|
+
const makeRun = (text, tabs, isLast) => ({
|
|
556
575
|
type: "run",
|
|
557
576
|
text,
|
|
558
577
|
style: runStyle,
|
|
559
578
|
...href ? { href } : {},
|
|
560
|
-
...lineBreak ? { lineBreak: true } : {},
|
|
579
|
+
...isLast && lineBreak ? { lineBreak: true } : {},
|
|
561
580
|
...tabs ? { tabs } : {}
|
|
562
|
-
};
|
|
563
|
-
return
|
|
581
|
+
});
|
|
582
|
+
if (segments.length <= 1) return makeRun(segments[0] ?? "", tabCount, true);
|
|
583
|
+
const interleave = tabCount === segments.length - 1;
|
|
584
|
+
return segments.map(
|
|
585
|
+
(seg, i) => makeRun(seg, i === 0 ? 0 : interleave ? 1 : i === 1 ? tabCount : 0, i === segments.length - 1)
|
|
586
|
+
);
|
|
564
587
|
}
|
|
565
588
|
function resolveHyperlinkHref(hl, ctx) {
|
|
566
589
|
const rId = hl.id;
|
|
@@ -677,7 +700,7 @@ async function parseParagraph(p, ctx, rawXml) {
|
|
|
677
700
|
else {
|
|
678
701
|
for (const ir of innerRuns) {
|
|
679
702
|
const run2 = await parseRun(ir, paraStyle, ctx);
|
|
680
|
-
if (run2 !== null) runs.push(run2);
|
|
703
|
+
if (run2 !== null) runs.push(...Array.isArray(run2) ? run2 : [run2]);
|
|
681
704
|
}
|
|
682
705
|
}
|
|
683
706
|
continue;
|
|
@@ -705,9 +728,11 @@ async function parseParagraph(p, ctx, rawXml) {
|
|
|
705
728
|
if (fieldStack.some((f) => f.inResult && LIVE_FIELDS.has(f.name))) continue;
|
|
706
729
|
const run = await parseRun(r, paraStyle, ctx, href);
|
|
707
730
|
if (run !== null) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
731
|
+
for (const rn of Array.isArray(run) ? run : [run]) {
|
|
732
|
+
if (deleted && rn.type === "run") rn.deleted = true;
|
|
733
|
+
if (inserted && rn.type === "run") rn.inserted = true;
|
|
734
|
+
runs.push(rn);
|
|
735
|
+
}
|
|
711
736
|
}
|
|
712
737
|
}
|
|
713
738
|
const segments = [[]];
|
|
@@ -1268,12 +1293,19 @@ function renderRun(run, parent, skipLeadingTabs = false) {
|
|
|
1268
1293
|
target = a;
|
|
1269
1294
|
}
|
|
1270
1295
|
if (run.type === "image") {
|
|
1296
|
+
const ir = run;
|
|
1271
1297
|
const img = document.createElement("img");
|
|
1272
|
-
setImageSrc(img,
|
|
1273
|
-
img.width =
|
|
1274
|
-
img.height =
|
|
1298
|
+
setImageSrc(img, ir.src);
|
|
1299
|
+
img.width = ir.widthPx;
|
|
1300
|
+
img.height = ir.heightPx;
|
|
1275
1301
|
img.style.display = "inline-block";
|
|
1276
1302
|
img.style.maxWidth = "100%";
|
|
1303
|
+
if (inHeaderFooter && ir.anchorXPx !== void 0) {
|
|
1304
|
+
img.style.position = "absolute";
|
|
1305
|
+
img.style.left = `${ir.anchorXPx}px`;
|
|
1306
|
+
img.style.top = `${ir.anchorYPx ?? 0}px`;
|
|
1307
|
+
img.style.maxWidth = "none";
|
|
1308
|
+
}
|
|
1277
1309
|
target.appendChild(img);
|
|
1278
1310
|
return;
|
|
1279
1311
|
}
|
|
@@ -1386,6 +1418,7 @@ function ensureLineBox(el) {
|
|
|
1386
1418
|
}
|
|
1387
1419
|
}
|
|
1388
1420
|
var inTableCell = false;
|
|
1421
|
+
var inHeaderFooter = false;
|
|
1389
1422
|
var currentPageNumber = 0;
|
|
1390
1423
|
var FOOTNOTE_SEPARATOR_H = 12;
|
|
1391
1424
|
function footnoteNumbersIn(el) {
|
|
@@ -1434,7 +1467,10 @@ function renderFooter(doc, pageDiv, pageNum, pm, footerPx) {
|
|
|
1434
1467
|
const el = document.createElement("div");
|
|
1435
1468
|
el.className = "ssd-footer";
|
|
1436
1469
|
el.style.cssText = `position:absolute;left:${pm.left}px;right:${pm.right}px;bottom:${footerPx}px`;
|
|
1470
|
+
const prevHF = inHeaderFooter;
|
|
1471
|
+
inHeaderFooter = true;
|
|
1437
1472
|
renderBlocks(footer, el);
|
|
1473
|
+
inHeaderFooter = prevHF;
|
|
1438
1474
|
pageDiv.appendChild(el);
|
|
1439
1475
|
currentPageNumber = prev;
|
|
1440
1476
|
}
|
|
@@ -1451,7 +1487,10 @@ function renderHeader(doc, pageDiv, pageNum, pm, headerPx) {
|
|
|
1451
1487
|
const el = document.createElement("div");
|
|
1452
1488
|
el.className = "ssd-header";
|
|
1453
1489
|
el.style.cssText = `position:absolute;left:${pm.left}px;right:${pm.right}px;top:${headerPx}px`;
|
|
1490
|
+
const prevHF = inHeaderFooter;
|
|
1491
|
+
inHeaderFooter = true;
|
|
1454
1492
|
renderBlocks(header, el);
|
|
1493
|
+
inHeaderFooter = prevHF;
|
|
1455
1494
|
pageDiv.appendChild(el);
|
|
1456
1495
|
currentPageNumber = prev;
|
|
1457
1496
|
}
|
|
@@ -1478,8 +1517,14 @@ function renderParagraph(block) {
|
|
|
1478
1517
|
const p = document.createElement("p");
|
|
1479
1518
|
p.style.cssText = paragraphCss(block.style);
|
|
1480
1519
|
if (block.style.rtl) p.dir = "rtl";
|
|
1481
|
-
|
|
1520
|
+
let stops = block.style.tabStops;
|
|
1482
1521
|
const hasTabRun = block.runs.some((r) => r.type === "run" && (r.tabs ?? 0) > 0);
|
|
1522
|
+
if (inHeaderFooter && hasTabRun && !stops?.some((s) => s.val === "right" || s.val === "center" || s.val === "decimal")) {
|
|
1523
|
+
stops = [
|
|
1524
|
+
{ posPx: 0, val: "center", leader: "none" },
|
|
1525
|
+
{ posPx: 0, val: "right", leader: "none" }
|
|
1526
|
+
];
|
|
1527
|
+
}
|
|
1483
1528
|
const alignStop = stops?.some((s) => s.val === "right" || s.val === "center" || s.val === "decimal");
|
|
1484
1529
|
if (hasTabRun && stops && alignStop) {
|
|
1485
1530
|
renderTabbedParagraph(block, p, stops);
|
|
@@ -1594,6 +1639,12 @@ var LIST_STYLE = {
|
|
|
1594
1639
|
bullet: "disc",
|
|
1595
1640
|
none: "none"
|
|
1596
1641
|
};
|
|
1642
|
+
function listStyleTypeFor(format, bulletText) {
|
|
1643
|
+
if (format === "bullet" && bulletText && /^[\x20-\x7E]+$/.test(bulletText)) {
|
|
1644
|
+
return `"${bulletText.replace(/[\\"]/g, "\\$&")}"`;
|
|
1645
|
+
}
|
|
1646
|
+
return LIST_STYLE[format];
|
|
1647
|
+
}
|
|
1597
1648
|
function renderBlocks(blocks, container) {
|
|
1598
1649
|
let stack = [];
|
|
1599
1650
|
const closeLists = () => {
|
|
@@ -1602,13 +1653,13 @@ function renderBlocks(blocks, container) {
|
|
|
1602
1653
|
};
|
|
1603
1654
|
for (const block of blocks) {
|
|
1604
1655
|
if (block.type === "paragraph" && block.list) {
|
|
1605
|
-
const { numId, ordered, start, ilvl, format } = block.list;
|
|
1656
|
+
const { numId, ordered, start, ilvl, format, bulletText } = block.list;
|
|
1606
1657
|
if (stack.length > 0 && stack[0].numId !== numId) closeLists();
|
|
1607
1658
|
while (stack.length > ilvl + 1) stack.pop();
|
|
1608
1659
|
while (stack.length < ilvl + 1) {
|
|
1609
1660
|
const listEl = document.createElement(ordered ? "ol" : "ul");
|
|
1610
1661
|
if (ordered) listEl.start = start;
|
|
1611
|
-
const styleType =
|
|
1662
|
+
const styleType = listStyleTypeFor(format, bulletText);
|
|
1612
1663
|
if (styleType) listEl.style.listStyleType = styleType;
|
|
1613
1664
|
const indent = block.style.indentLeft ?? 24;
|
|
1614
1665
|
listEl.style.margin = "0";
|
|
@@ -1721,25 +1772,29 @@ function renderPlainPaginated(doc, container, pw, ph, pm, pageOffset = 0) {
|
|
|
1721
1772
|
for (; ; ) {
|
|
1722
1773
|
pageDiv.appendChild(table);
|
|
1723
1774
|
const th = table.offsetHeight;
|
|
1724
|
-
|
|
1775
|
+
const pieceRefs = footnoteNumbersIn(table);
|
|
1776
|
+
const pieceReserve = fnReserve(pieceRefs, pageHasFn);
|
|
1777
|
+
if (used + th <= contentH - reserve - pieceReserve) {
|
|
1725
1778
|
used += th;
|
|
1779
|
+
recordFootnotes(pieceRefs, pieceReserve);
|
|
1726
1780
|
break;
|
|
1727
1781
|
}
|
|
1728
|
-
const rest = splitTableRows(table, contentH - reserve - used);
|
|
1782
|
+
const rest = splitTableRows(table, contentH - reserve - pieceReserve - used);
|
|
1729
1783
|
if (!rest) {
|
|
1730
1784
|
if (used === 0) {
|
|
1731
1785
|
used += th;
|
|
1786
|
+
recordFootnotes(pieceRefs, pieceReserve);
|
|
1732
1787
|
break;
|
|
1733
1788
|
}
|
|
1734
1789
|
pageDiv.removeChild(table);
|
|
1735
1790
|
pageDiv = newPage();
|
|
1736
1791
|
continue;
|
|
1737
1792
|
}
|
|
1793
|
+
const fitRefs = footnoteNumbersIn(table);
|
|
1794
|
+
recordFootnotes(fitRefs, fnReserve(fitRefs, pageHasFn));
|
|
1738
1795
|
pageDiv = newPage();
|
|
1739
1796
|
table = rest;
|
|
1740
1797
|
}
|
|
1741
|
-
const refs2 = footnoteNumbersIn(table);
|
|
1742
|
-
recordFootnotes(refs2, fnReserve(refs2, pageHasFn));
|
|
1743
1798
|
continue;
|
|
1744
1799
|
}
|
|
1745
1800
|
const h = heights[i];
|
|
@@ -2198,8 +2253,13 @@ async function resolvePart(documentXml, relationshipMap, zip, ctx, kind, parseXm
|
|
|
2198
2253
|
}
|
|
2199
2254
|
async function resolveRid(rId, relationshipMap, zip, ctx, parseXml) {
|
|
2200
2255
|
if (!rId || !relationshipMap[rId]) return void 0;
|
|
2201
|
-
const
|
|
2202
|
-
|
|
2256
|
+
const target = relationshipMap[rId].target;
|
|
2257
|
+
const xml = await readEntry(zip, `word/${target}`, false);
|
|
2258
|
+
if (!xml) return void 0;
|
|
2259
|
+
const base = target.replace(/^.*\//, "");
|
|
2260
|
+
const partRelsXml = await readEntry(zip, `word/_rels/${base}.rels`, false);
|
|
2261
|
+
const partCtx = partRelsXml ? { ...ctx, relationshipMap: parseRelationships(partRelsXml) } : ctx;
|
|
2262
|
+
return parseXml(xml, partCtx);
|
|
2203
2263
|
}
|
|
2204
2264
|
async function resolveNotes(xml, kind, refIds, ctx) {
|
|
2205
2265
|
if (!xml || refIds.length === 0) return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-super-doc",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "Faithful DOCX renderer with exposed typed IR for building document editors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,16 +13,29 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"sideEffects": false,
|
|
16
|
-
"files": [
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
17
19
|
"scripts": {
|
|
18
20
|
"build": "tsup src/index.ts --format esm --dts",
|
|
19
21
|
"build:demo": "tsup src/index.ts --format iife --global-name SimpleDoc --outDir demo",
|
|
20
22
|
"typecheck": "tsc --noEmit",
|
|
21
23
|
"test": "vitest run",
|
|
22
24
|
"test:watch": "vitest",
|
|
23
|
-
"
|
|
25
|
+
"test:browser": "vitest run --config vitest.browser.config.ts",
|
|
26
|
+
"prepublishOnly": "npm run build && npm run typecheck && npm test",
|
|
27
|
+
"postversion": "git push origin HEAD --follow-tags"
|
|
24
28
|
},
|
|
25
|
-
"keywords": [
|
|
29
|
+
"keywords": [
|
|
30
|
+
"docx",
|
|
31
|
+
"word",
|
|
32
|
+
"renderer",
|
|
33
|
+
"parser",
|
|
34
|
+
"document",
|
|
35
|
+
"ir",
|
|
36
|
+
"ooxml",
|
|
37
|
+
"html"
|
|
38
|
+
],
|
|
26
39
|
"license": "MIT",
|
|
27
40
|
"author": "Rafael-T-Santos <dev.rafaelsantos@outlook.com>",
|
|
28
41
|
"repository": {
|
|
@@ -39,6 +52,8 @@
|
|
|
39
52
|
},
|
|
40
53
|
"devDependencies": {
|
|
41
54
|
"@types/node": "^26.0.1",
|
|
55
|
+
"esbuild": "^0.27.7",
|
|
56
|
+
"playwright-core": "^1.61.1",
|
|
42
57
|
"tsup": "^8.5.1",
|
|
43
58
|
"typescript": "^6.0.3",
|
|
44
59
|
"vitest": "^4.1.9"
|