docgen-utils 1.0.18 → 1.0.20

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 (74) hide show
  1. package/README.md +63 -18
  2. package/dist/bundle.js +3466 -2528
  3. package/dist/bundle.min.js +242 -252
  4. package/dist/cli.js +3644 -2544
  5. package/dist/packages/cli/commands/export-docs.js +1 -1
  6. package/dist/packages/cli/commands/export-docs.js.map +1 -1
  7. package/dist/packages/cli/commands/export-slides.d.ts.map +1 -1
  8. package/dist/packages/cli/commands/export-slides.js +11 -7
  9. package/dist/packages/cli/commands/export-slides.js.map +1 -1
  10. package/dist/packages/cli/index.js.map +1 -1
  11. package/dist/packages/docs/common.d.ts +1 -0
  12. package/dist/packages/docs/common.d.ts.map +1 -1
  13. package/dist/packages/docs/convert.d.ts.map +1 -1
  14. package/dist/packages/docs/convert.js +6 -15
  15. package/dist/packages/docs/convert.js.map +1 -1
  16. package/dist/packages/docs/import-docx.d.ts.map +1 -1
  17. package/dist/packages/docs/import-docx.js +168 -82
  18. package/dist/packages/docs/import-docx.js.map +1 -1
  19. package/dist/packages/docs/parse-colors.d.ts +0 -5
  20. package/dist/packages/docs/parse-colors.d.ts.map +1 -1
  21. package/dist/packages/docs/parse-colors.js +2 -2
  22. package/dist/packages/docs/parse-colors.js.map +1 -1
  23. package/dist/packages/docs/parse-css.d.ts +0 -9
  24. package/dist/packages/docs/parse-css.d.ts.map +1 -1
  25. package/dist/packages/docs/parse-css.js +4 -6
  26. package/dist/packages/docs/parse-css.js.map +1 -1
  27. package/dist/packages/docs/parse-helpers.d.ts +0 -1
  28. package/dist/packages/docs/parse-helpers.d.ts.map +1 -1
  29. package/dist/packages/docs/parse-helpers.js +1 -1
  30. package/dist/packages/docs/parse-helpers.js.map +1 -1
  31. package/dist/packages/docs/parse-inline.d.ts +0 -13
  32. package/dist/packages/docs/parse-inline.d.ts.map +1 -1
  33. package/dist/packages/docs/parse-inline.js +7 -7
  34. package/dist/packages/docs/parse-inline.js.map +1 -1
  35. package/dist/packages/docs/parse-layout.d.ts.map +1 -1
  36. package/dist/packages/docs/parse-layout.js +1 -14
  37. package/dist/packages/docs/parse-layout.js.map +1 -1
  38. package/dist/packages/docs/parse-special.js +1 -1
  39. package/dist/packages/docs/parse-special.js.map +1 -1
  40. package/dist/packages/docs/parse.d.ts.map +1 -1
  41. package/dist/packages/docs/parse.js +76 -30
  42. package/dist/packages/docs/parse.js.map +1 -1
  43. package/dist/packages/shared/fetch-with-proxy.d.ts +13 -8
  44. package/dist/packages/shared/fetch-with-proxy.d.ts.map +1 -1
  45. package/dist/packages/shared/fetch-with-proxy.js +189 -22
  46. package/dist/packages/shared/fetch-with-proxy.js.map +1 -1
  47. package/dist/packages/shared/zip-guard.d.ts +37 -0
  48. package/dist/packages/shared/zip-guard.d.ts.map +1 -0
  49. package/dist/packages/shared/zip-guard.js +101 -0
  50. package/dist/packages/shared/zip-guard.js.map +1 -0
  51. package/dist/packages/slides/convert.d.ts +1 -3
  52. package/dist/packages/slides/convert.d.ts.map +1 -1
  53. package/dist/packages/slides/convert.js +8 -74
  54. package/dist/packages/slides/convert.js.map +1 -1
  55. package/dist/packages/slides/createPresentation.d.ts +1 -1
  56. package/dist/packages/slides/createPresentation.d.ts.map +1 -1
  57. package/dist/packages/slides/createPresentation.js +1 -10
  58. package/dist/packages/slides/createPresentation.js.map +1 -1
  59. package/dist/packages/slides/import-pptx.d.ts.map +1 -1
  60. package/dist/packages/slides/import-pptx.js +5 -7
  61. package/dist/packages/slides/import-pptx.js.map +1 -1
  62. package/dist/packages/slides/parse.d.ts +0 -22
  63. package/dist/packages/slides/parse.d.ts.map +1 -1
  64. package/dist/packages/slides/parse.js +38 -42
  65. package/dist/packages/slides/parse.js.map +1 -1
  66. package/dist/packages/slides/transform.d.ts.map +1 -1
  67. package/dist/packages/slides/transform.js +1 -5
  68. package/dist/packages/slides/transform.js.map +1 -1
  69. package/dist/packages/slides/vendor/VENDORING.md +2 -2
  70. package/package.json +16 -10
  71. package/dist/packages/cli/commands/common.d.ts +0 -2
  72. package/dist/packages/cli/commands/common.d.ts.map +0 -1
  73. package/dist/packages/cli/commands/common.js +0 -22
  74. package/dist/packages/cli/commands/common.js.map +0 -1
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Usage: const html = await importDocx(arrayBuffer);
6
6
  */
7
- import JSZip from "jszip";
7
+ import { loadZipSafely } from "../shared/zip-guard";
8
8
  import { EMFJS } from "rtf.js";
9
9
  import { parseHTML } from "linkedom";
10
10
  // ============================================================================
@@ -194,7 +194,7 @@ function readEmfHeader(buffer) {
194
194
  frameHeight: frameBottom
195
195
  };
196
196
  }
197
- // For backward compatibility
197
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
198
198
  function readEmfBounds(buffer) {
199
199
  const header = readEmfHeader(buffer);
200
200
  return { width: header.boundsWidth, height: header.boundsHeight };
@@ -332,11 +332,11 @@ function escapeSvgText(text) {
332
332
  /**
333
333
  * Convert EMF buffer to SVG for inline rendering
334
334
  */
335
- function convertEmfToSvg(buffer) {
335
+ async function convertEmfToSvg(buffer) {
336
336
  try {
337
337
  setupEmfDom();
338
338
  const header = readEmfHeader(buffer);
339
- const { boundsWidth, boundsHeight, frameWidth, frameHeight } = header;
339
+ const { boundsWidth, boundsHeight } = header;
340
340
  // Disable EMFJS logging by temporarily overriding console
341
341
  const originalLog = console.log;
342
342
  console.log = () => { }; // Silence EMFJS debug logs
@@ -538,6 +538,7 @@ function parseThemeFonts(themeDoc) {
538
538
  }
539
539
  function parseStyles(stylesDoc, themeColors, themeFonts) {
540
540
  const styles = new Map();
541
+ const tableStyles = new Map();
541
542
  const defaults = {};
542
543
  // Parse document defaults (w:docDefaults)
543
544
  const docDefaults = stylesDoc.getElementsByTagName("w:docDefaults")[0];
@@ -615,8 +616,27 @@ function parseStyles(stylesDoc, themeColors, themeFonts) {
615
616
  const pPr = pPrEl ? parseParagraphProps(pPrEl) : undefined;
616
617
  const rPr = rPrEl ? parseRunProps(rPrEl, themeColors, themeFonts) : undefined;
617
618
  styles.set(styleId, { basedOn, pPr, rPr, name });
619
+ // Parse table styles (w:type="table") for border and cell margin info
620
+ const styleType = styleEl.getAttribute("w:type");
621
+ if (styleType === "table") {
622
+ const tblPrEl = findChild(styleEl, "tblPr");
623
+ const tblStyleProps = {};
624
+ if (tblPrEl) {
625
+ // Parse table borders from style
626
+ const tblBordersEl = findChild(tblPrEl, "tblBorders");
627
+ if (tblBordersEl) {
628
+ tblStyleProps.borders = parseBorders(tblBordersEl, themeColors);
629
+ }
630
+ // Parse default cell margins from style
631
+ const tblCellMarEl = findChild(tblPrEl, "tblCellMar");
632
+ if (tblCellMarEl) {
633
+ tblStyleProps.cellMargins = parseCellMargins(tblCellMarEl);
634
+ }
635
+ }
636
+ tableStyles.set(styleId, { basedOn, tblPr: tblStyleProps });
637
+ }
618
638
  }
619
- return { styles, defaults };
639
+ return { styles, defaults, tableStyles };
620
640
  }
621
641
  /**
622
642
  * Parse section properties from sectPr element.
@@ -765,7 +785,7 @@ function parsePositionedElement(drawing, themeColors, themeFonts) {
765
785
  const stops = [];
766
786
  const gsElements = findChildren(gsLst, "gs");
767
787
  for (const gs of gsElements) {
768
- const pos = parseInt(gs.getAttribute("pos") ?? "0", 10) / 1000; // Convert from 100000ths to percentage
788
+ const pos = parseInt(gs.getAttribute("pos") ?? "0", 10) / 1000; // Convert from thousandths of a percent (0-100000) to percentage (0-100)
769
789
  let color = "#000000";
770
790
  const srgbClr = findChild(gs, "srgbClr");
771
791
  const schemeClr = findChild(gs, "schemeClr");
@@ -1339,7 +1359,7 @@ function parseParagraph(p, themeColors, themeFonts) {
1339
1359
  processElement(p);
1340
1360
  return { runs, props };
1341
1361
  }
1342
- function parseTableCell(tc, themeColors, themeFonts) {
1362
+ function parseTableCell(tc, themeColors, themeFonts, tableStyles) {
1343
1363
  const cell = { paragraphs: [] };
1344
1364
  const tcPr = findChild(tc, "tcPr");
1345
1365
  if (tcPr) {
@@ -1387,6 +1407,11 @@ function parseTableCell(tc, themeColors, themeFonts) {
1387
1407
  if (noWrap) {
1388
1408
  cell.noWrap = true;
1389
1409
  }
1410
+ // Cell-level margins (w:tcMar) - override table-level cell margins
1411
+ const tcMar = findChild(tcPr, "tcMar");
1412
+ if (tcMar) {
1413
+ cell.cellMargins = parseCellMargins(tcMar);
1414
+ }
1390
1415
  }
1391
1416
  // Parse paragraphs in cell (including those inside SDT elements)
1392
1417
  const collectParagraphs = (parent) => {
@@ -1430,10 +1455,43 @@ function parseTableCell(tc, themeColors, themeFonts) {
1430
1455
  };
1431
1456
  const nestedTables = collectTables(tc);
1432
1457
  if (nestedTables.length > 0) {
1433
- cell.nestedTables = nestedTables.map(tbl => parseTable(tbl, themeColors, themeFonts));
1458
+ cell.nestedTables = nestedTables.map(tbl => parseTable(tbl, themeColors, themeFonts, tableStyles));
1434
1459
  }
1435
1460
  return cell;
1436
1461
  }
1462
+ /**
1463
+ * Parse cell margins from a w:tblCellMar or w:tcMar element.
1464
+ * Returns margins in twips.
1465
+ */
1466
+ function parseCellMargins(marEl) {
1467
+ const margins = {};
1468
+ const topEl = findChild(marEl, "top");
1469
+ const bottomEl = findChild(marEl, "bottom");
1470
+ // Note: OOXML uses "start"/"end" in newer docs, "left"/"right" in older ones
1471
+ const leftEl = findChild(marEl, "left") ?? findChild(marEl, "start");
1472
+ const rightEl = findChild(marEl, "right") ?? findChild(marEl, "end");
1473
+ if (topEl) {
1474
+ const w = topEl.getAttribute("w:w");
1475
+ if (w)
1476
+ margins.top = parseInt(w, 10);
1477
+ }
1478
+ if (bottomEl) {
1479
+ const w = bottomEl.getAttribute("w:w");
1480
+ if (w)
1481
+ margins.bottom = parseInt(w, 10);
1482
+ }
1483
+ if (leftEl) {
1484
+ const w = leftEl.getAttribute("w:w");
1485
+ if (w)
1486
+ margins.left = parseInt(w, 10);
1487
+ }
1488
+ if (rightEl) {
1489
+ const w = rightEl.getAttribute("w:w");
1490
+ if (w)
1491
+ margins.right = parseInt(w, 10);
1492
+ }
1493
+ return margins;
1494
+ }
1437
1495
  function parseBorders(bordersEl, themeColors) {
1438
1496
  const borders = {};
1439
1497
  const parseBorder = (el) => {
@@ -1465,14 +1523,37 @@ function parseBorders(bordersEl, themeColors) {
1465
1523
  borders.insideV = parseBorder(findChild(bordersEl, "insideV"));
1466
1524
  return borders;
1467
1525
  }
1468
- function parseTable(tbl, themeColors, themeFonts) {
1526
+ function parseTable(tbl, themeColors, themeFonts, tableStyles) {
1469
1527
  const table = { rows: [] };
1470
1528
  const tblPr = findChild(tbl, "tblPr");
1471
1529
  if (tblPr) {
1530
+ // Resolve table style (w:tblStyle) - this provides default borders, cell margins, etc.
1531
+ const tblStyleEl = findChild(tblPr, "tblStyle");
1532
+ let resolvedStyleProps;
1533
+ if (tblStyleEl && tableStyles) {
1534
+ const styleId = tblStyleEl.getAttribute("w:val");
1535
+ if (styleId) {
1536
+ resolvedStyleProps = resolveTableStyle(styleId, tableStyles);
1537
+ }
1538
+ }
1539
+ // Parse explicit table borders (override style borders)
1472
1540
  const tblBorders = findChild(tblPr, "tblBorders");
1473
1541
  if (tblBorders) {
1474
1542
  table.borders = parseBorders(tblBorders, themeColors);
1475
1543
  }
1544
+ else if (resolvedStyleProps?.borders) {
1545
+ // Fall back to style-defined borders
1546
+ table.borders = resolvedStyleProps.borders;
1547
+ }
1548
+ // Parse explicit cell margins from table properties
1549
+ const tblCellMar = findChild(tblPr, "tblCellMar");
1550
+ if (tblCellMar) {
1551
+ table.cellMargins = parseCellMargins(tblCellMar);
1552
+ }
1553
+ else if (resolvedStyleProps?.cellMargins) {
1554
+ // Fall back to style-defined cell margins
1555
+ table.cellMargins = resolvedStyleProps.cellMargins;
1556
+ }
1476
1557
  // Parse table width
1477
1558
  const tblW = findChild(tblPr, "tblW");
1478
1559
  if (tblW) {
@@ -1491,35 +1572,6 @@ function parseTable(tbl, themeColors, themeFonts) {
1491
1572
  table.tableIndent = parseInt(w, 10);
1492
1573
  }
1493
1574
  }
1494
- // Parse table cell margins (default margins for all cells)
1495
- const tblCellMar = findChild(tblPr, "tblCellMar");
1496
- if (tblCellMar) {
1497
- table.cellMargins = {};
1498
- const topMar = findChild(tblCellMar, "top");
1499
- const bottomMar = findChild(tblCellMar, "bottom");
1500
- const leftMar = findChild(tblCellMar, "left") || findChild(tblCellMar, "start");
1501
- const rightMar = findChild(tblCellMar, "right") || findChild(tblCellMar, "end");
1502
- if (topMar) {
1503
- const w = topMar.getAttribute("w:w");
1504
- if (w)
1505
- table.cellMargins.top = parseInt(w, 10);
1506
- }
1507
- if (bottomMar) {
1508
- const w = bottomMar.getAttribute("w:w");
1509
- if (w)
1510
- table.cellMargins.bottom = parseInt(w, 10);
1511
- }
1512
- if (leftMar) {
1513
- const w = leftMar.getAttribute("w:w");
1514
- if (w)
1515
- table.cellMargins.left = parseInt(w, 10);
1516
- }
1517
- if (rightMar) {
1518
- const w = rightMar.getAttribute("w:w");
1519
- if (w)
1520
- table.cellMargins.right = parseInt(w, 10);
1521
- }
1522
- }
1523
1575
  }
1524
1576
  // Parse column widths from tblGrid
1525
1577
  const tblGrid = findChild(tbl, "tblGrid");
@@ -1556,12 +1608,43 @@ function parseTable(tbl, themeColors, themeFonts) {
1556
1608
  }
1557
1609
  const tcs = findChildren(tr, "tc");
1558
1610
  for (const tc of tcs) {
1559
- row.cells.push(parseTableCell(tc, themeColors, themeFonts));
1611
+ row.cells.push(parseTableCell(tc, themeColors, themeFonts, tableStyles));
1560
1612
  }
1561
1613
  table.rows.push(row);
1562
1614
  }
1563
1615
  return table;
1564
1616
  }
1617
+ /**
1618
+ * Resolve a table style by walking the basedOn chain.
1619
+ * Merges borders and cell margins from parent styles (parent first, child overrides).
1620
+ */
1621
+ function resolveTableStyle(styleId, tableStyles) {
1622
+ const visited = new Set();
1623
+ const chain = [];
1624
+ let currentId = styleId;
1625
+ while (currentId && !visited.has(currentId)) {
1626
+ visited.add(currentId);
1627
+ const style = tableStyles.get(currentId);
1628
+ if (!style)
1629
+ break;
1630
+ if (style.tblPr)
1631
+ chain.unshift(style.tblPr); // parent first
1632
+ currentId = style.basedOn;
1633
+ }
1634
+ if (chain.length === 0)
1635
+ return undefined;
1636
+ // Merge: start from parent, child overrides
1637
+ const merged = {};
1638
+ for (const props of chain) {
1639
+ if (props.borders) {
1640
+ merged.borders = { ...merged.borders, ...props.borders };
1641
+ }
1642
+ if (props.cellMargins) {
1643
+ merged.cellMargins = { ...merged.cellMargins, ...props.cellMargins };
1644
+ }
1645
+ }
1646
+ return merged;
1647
+ }
1565
1648
  function parseDrawing(drawing, themeColors, inlineOnly = false) {
1566
1649
  // Look for inline or anchor images
1567
1650
  const inline = findDescendant(drawing, "inline");
@@ -1738,7 +1821,7 @@ function parseDrawing(drawing, themeColors, inlineOnly = false) {
1738
1821
  },
1739
1822
  };
1740
1823
  }
1741
- function parseDocument(docDoc, themeColors, themeFonts) {
1824
+ function parseDocument(docDoc, themeColors, themeFonts, tableStyles) {
1742
1825
  const elements = [];
1743
1826
  const positionedElements = [];
1744
1827
  const body = docDoc.getElementsByTagName("w:body")[0];
@@ -1766,7 +1849,7 @@ function parseDocument(docDoc, themeColors, themeFonts) {
1766
1849
  }
1767
1850
  }
1768
1851
  else if (el.localName === "tbl") {
1769
- elements.push({ kind: "table", data: parseTable(el, themeColors, themeFonts) });
1852
+ elements.push({ kind: "table", data: parseTable(el, themeColors, themeFonts, tableStyles) });
1770
1853
  }
1771
1854
  else if (el.localName === "sdt") {
1772
1855
  // Structured document tag - process its content
@@ -1881,14 +1964,13 @@ function renderParagraphToHtml(para, styleMap, numberingMap, imageMap = new Map(
1881
1964
  }
1882
1965
  // Determine HTML tag based on style
1883
1966
  let tag = "p";
1884
- let headingLevel = 0;
1885
1967
  let isTitleStyle = false;
1886
1968
  let isSubtitleStyle = false;
1887
1969
  if (props.styleId) {
1888
1970
  const styleName = styleMap.get(props.styleId)?.name?.toLowerCase() ?? props.styleId.toLowerCase();
1889
1971
  const headingMatch = styleName.match(/heading\s*(\d)/i);
1890
1972
  if (headingMatch) {
1891
- headingLevel = parseInt(headingMatch[1], 10);
1973
+ const headingLevel = parseInt(headingMatch[1], 10);
1892
1974
  if (headingLevel >= 1 && headingLevel <= 6) {
1893
1975
  tag = `h${headingLevel}`;
1894
1976
  }
@@ -2058,8 +2140,6 @@ function renderTableToHtml(table, styleMap, numberingMap, themeColors, imageMap,
2058
2140
  colIdx += cell.gridSpan ?? 1;
2059
2141
  }
2060
2142
  }
2061
- // Track column index for applying widths
2062
- let colIndex = 0;
2063
2143
  for (let rowIdx = 0; rowIdx < table.rows.length; rowIdx++) {
2064
2144
  const row = table.rows[rowIdx];
2065
2145
  const rowStyles = [];
@@ -2069,8 +2149,8 @@ function renderTableToHtml(table, styleMap, numberingMap, themeColors, imageMap,
2069
2149
  html += `<tr${rowStyles.length > 0 ? ` style="${rowStyles.join(";")}"` : ""}>`;
2070
2150
  // First pass: find rowspan cells and calculate empty paragraphs to distribute
2071
2151
  // We subtract the number of content lines in subsequent rows covered by the rowspan
2072
- let emptyParagraphsToDistribute = [];
2073
- colIndex = 0;
2152
+ const emptyParagraphsToDistribute = [];
2153
+ let colIndex = 0;
2074
2154
  for (const cell of row.cells) {
2075
2155
  if (cell.vMerge === "continue") {
2076
2156
  colIndex += cell.gridSpan ?? 1;
@@ -2094,7 +2174,6 @@ function renderTableToHtml(table, styleMap, numberingMap, themeColors, imageMap,
2094
2174
  // Find the cell at the same column in this row (it should be vMerge="continue")
2095
2175
  // But we need to count lines from OTHER cells in that row
2096
2176
  // Actually, we just need to count paragraphs in any non-continue cell in row r
2097
- let colIdx = 0;
2098
2177
  for (const nextRowCell of table.rows[r].cells) {
2099
2178
  if (nextRowCell.vMerge !== "continue") {
2100
2179
  // Count non-empty paragraphs in this cell
@@ -2107,7 +2186,6 @@ function renderTableToHtml(table, styleMap, numberingMap, themeColors, imageMap,
2107
2186
  }
2108
2187
  break; // Only count from first non-continue cell
2109
2188
  }
2110
- colIdx += nextRowCell.gridSpan ?? 1;
2111
2189
  }
2112
2190
  }
2113
2191
  // Distribute: emptyParagraphs - linesInNextRows
@@ -2128,13 +2206,14 @@ function renderTableToHtml(table, styleMap, numberingMap, themeColors, imageMap,
2128
2206
  }
2129
2207
  const cellStyles = [];
2130
2208
  const cellAttrs = [];
2131
- // Apply cell padding: use extracted table cell margins if available
2132
- // Priority: 1) table-level margins, 2) docDefaults from TableNormal style, 3) Word defaults
2133
- if (table.cellMargins) {
2134
- const top = table.cellMargins.top !== undefined ? twipsToPx(table.cellMargins.top) : 0;
2135
- const right = table.cellMargins.right !== undefined ? twipsToPx(table.cellMargins.right) : 0;
2136
- const bottom = table.cellMargins.bottom !== undefined ? twipsToPx(table.cellMargins.bottom) : 0;
2137
- const left = table.cellMargins.left !== undefined ? twipsToPx(table.cellMargins.left) : 0;
2209
+ // Apply cell padding: use extracted cell margins if available
2210
+ // Priority: 1) cell-level margins (tcMar), 2) table-level margins, 3) docDefaults from TableNormal style, 4) Word defaults
2211
+ const margins = cell.cellMargins ?? table.cellMargins;
2212
+ if (margins) {
2213
+ const top = margins.top !== undefined ? twipsToPx(margins.top) : 0;
2214
+ const right = margins.right !== undefined ? twipsToPx(margins.right) : 0;
2215
+ const bottom = margins.bottom !== undefined ? twipsToPx(margins.bottom) : 0;
2216
+ const left = margins.left !== undefined ? twipsToPx(margins.left) : 0;
2138
2217
  cellStyles.push(`padding:${top}px ${right}px ${bottom}px ${left}px`);
2139
2218
  }
2140
2219
  else if (docDefaults.tableCellMargins) {
@@ -2199,26 +2278,32 @@ function renderTableToHtml(table, styleMap, numberingMap, themeColors, imageMap,
2199
2278
  }
2200
2279
  }
2201
2280
  else if (table.borders) {
2202
- // Fall back to table borders
2203
- if (table.borders.top) {
2204
- cellStyles.push(`border-top:${table.borders.top.size}pt ${getBorderStyleCss(table.borders.top.style)} #${table.borders.top.color}`);
2205
- }
2206
- if (table.borders.bottom) {
2207
- cellStyles.push(`border-bottom:${table.borders.bottom.size}pt ${getBorderStyleCss(table.borders.bottom.style)} #${table.borders.bottom.color}`);
2208
- }
2209
- if (table.borders.left) {
2210
- cellStyles.push(`border-left:${table.borders.left.size}pt ${getBorderStyleCss(table.borders.left.style)} #${table.borders.left.color}`);
2211
- }
2212
- if (table.borders.right) {
2213
- cellStyles.push(`border-right:${table.borders.right.size}pt ${getBorderStyleCss(table.borders.right.style)} #${table.borders.right.color}`);
2214
- }
2215
- if (table.borders.insideH) {
2216
- cellStyles.push(`border-top:${table.borders.insideH.size}pt ${getBorderStyleCss(table.borders.insideH.style)} #${table.borders.insideH.color}`);
2217
- cellStyles.push(`border-bottom:${table.borders.insideH.size}pt ${getBorderStyleCss(table.borders.insideH.style)} #${table.borders.insideH.color}`);
2218
- }
2219
- if (table.borders.insideV) {
2220
- cellStyles.push(`border-left:${table.borders.insideV.size}pt ${getBorderStyleCss(table.borders.insideV.style)} #${table.borders.insideV.color}`);
2221
- cellStyles.push(`border-right:${table.borders.insideV.size}pt ${getBorderStyleCss(table.borders.insideV.style)} #${table.borders.insideV.color}`);
2281
+ // Fall back to table borders: use insideH/insideV for interior, edge borders for outer edges
2282
+ const rowIndex = table.rows.indexOf(row);
2283
+ const cellIndex = row.cells.filter(c => c.vMerge !== "continue").indexOf(cell);
2284
+ const isFirstRow = rowIndex === 0;
2285
+ const isLastRow = rowIndex === table.rows.length - 1;
2286
+ const isFirstCol = cellIndex === 0;
2287
+ const isLastCol = cellIndex === row.cells.filter(c => c.vMerge !== "continue").length - 1;
2288
+ // Top border: use table top border for first row, insideH for inner rows
2289
+ const topBorder = isFirstRow ? table.borders.top : table.borders.insideH;
2290
+ if (topBorder) {
2291
+ cellStyles.push(`border-top:${topBorder.size}pt ${getBorderStyleCss(topBorder.style)} #${topBorder.color}`);
2292
+ }
2293
+ // Bottom border: use table bottom border for last row, insideH for inner rows
2294
+ const bottomBorder = isLastRow ? table.borders.bottom : table.borders.insideH;
2295
+ if (bottomBorder) {
2296
+ cellStyles.push(`border-bottom:${bottomBorder.size}pt ${getBorderStyleCss(bottomBorder.style)} #${bottomBorder.color}`);
2297
+ }
2298
+ // Left border: use table left border for first col, insideV for inner cols
2299
+ const leftBorder = isFirstCol ? table.borders.left : table.borders.insideV;
2300
+ if (leftBorder) {
2301
+ cellStyles.push(`border-left:${leftBorder.size}pt ${getBorderStyleCss(leftBorder.style)} #${leftBorder.color}`);
2302
+ }
2303
+ // Right border: use table right border for last col, insideV for inner cols
2304
+ const rightBorder = isLastCol ? table.borders.right : table.borders.insideV;
2305
+ if (rightBorder) {
2306
+ cellStyles.push(`border-right:${rightBorder.size}pt ${getBorderStyleCss(rightBorder.style)} #${rightBorder.color}`);
2222
2307
  }
2223
2308
  }
2224
2309
  const attrsStr = cellAttrs.length > 0 ? " " + cellAttrs.join(" ") : "";
@@ -2303,7 +2388,7 @@ function renderImageToHtml(img, imageMap) {
2303
2388
  containerStyles.push(`border:${borderWidthPx}px solid #${img.border.color}`);
2304
2389
  }
2305
2390
  // Inject width/height into the SVG element to ensure proper sizing
2306
- let styledSvg = imageData.svgMarkup.replace(/<svg /, `<svg style="width:100%;height:100%;display:block;" `);
2391
+ const styledSvg = imageData.svgMarkup.replace(/<svg /, `<svg style="width:100%;height:100%;display:block;" `);
2307
2392
  return `<div style="${containerStyles.join(";")}">${styledSvg}</div>`;
2308
2393
  }
2309
2394
  // Regular image (dataUri type)
@@ -2634,7 +2719,7 @@ function generateStylesCss(styleMap, themeFonts) {
2634
2719
  * @returns HTML string representing the document
2635
2720
  */
2636
2721
  export default async function importDocx(arrayBuffer) {
2637
- const zip = await JSZip.loadAsync(arrayBuffer);
2722
+ const zip = await loadZipSafely(arrayBuffer);
2638
2723
  const parser = new DOMParser();
2639
2724
  // Parse document.xml
2640
2725
  const docXml = await zip.file("word/document.xml")?.async("text");
@@ -2652,6 +2737,7 @@ export default async function importDocx(arrayBuffer) {
2652
2737
  }
2653
2738
  // Parse styles - try multiple locations
2654
2739
  let styleMap = new Map();
2740
+ let tableStyleMap = new Map();
2655
2741
  let docDefaults = {};
2656
2742
  // Try word/styles.xml first (standard location)
2657
2743
  let stylesXml = await zip.file("word/styles.xml")?.async("text");
@@ -2663,6 +2749,7 @@ export default async function importDocx(arrayBuffer) {
2663
2749
  const stylesDoc = parser.parseFromString(stylesXml, "application/xml");
2664
2750
  const parsed = parseStyles(stylesDoc, themeColors, themeFonts);
2665
2751
  styleMap = parsed.styles;
2752
+ tableStyleMap = parsed.tableStyles;
2666
2753
  docDefaults = parsed.defaults;
2667
2754
  }
2668
2755
  // Parse numbering (for lists) - try multiple locations
@@ -2710,7 +2797,7 @@ export default async function importDocx(arrayBuffer) {
2710
2797
  // Handle EMF files specially - convert to SVG for web display
2711
2798
  if (ext === "emf") {
2712
2799
  const imgBuffer = await imgFile.async("arraybuffer");
2713
- const svgData = convertEmfToSvg(imgBuffer);
2800
+ const svgData = await convertEmfToSvg(imgBuffer);
2714
2801
  if (svgData) {
2715
2802
  imageMap.set(rId, svgData);
2716
2803
  }
@@ -2734,7 +2821,7 @@ export default async function importDocx(arrayBuffer) {
2734
2821
  // Parse headers and footers for positioned elements (decorative frames, etc.)
2735
2822
  const headers = [];
2736
2823
  const footers = [];
2737
- for (const [rId, filePath] of headerFooterRels) {
2824
+ for (const [, filePath] of headerFooterRels) {
2738
2825
  const hfXml = await zip.file(filePath)?.async("text");
2739
2826
  if (!hfXml)
2740
2827
  continue;
@@ -2754,7 +2841,7 @@ export default async function importDocx(arrayBuffer) {
2754
2841
  positionedElements.push(...header.positionedElements);
2755
2842
  }
2756
2843
  // Parse document elements and body positioned elements
2757
- const parsedDoc = parseDocument(docDoc, themeColors, themeFonts);
2844
+ const parsedDoc = parseDocument(docDoc, themeColors, themeFonts, tableStyleMap);
2758
2845
  const elements = parsedDoc.elements;
2759
2846
  // Add positioned elements from document body
2760
2847
  positionedElements.push(...parsedDoc.positionedElements);
@@ -2767,7 +2854,6 @@ export default async function importDocx(arrayBuffer) {
2767
2854
  const marginRightPx = sectionProps.marginRight ? twipsToPx(sectionProps.marginRight) : 72;
2768
2855
  const marginTopPx = sectionProps.marginTop ? twipsToPx(sectionProps.marginTop) : 72;
2769
2856
  const marginBottomPx = sectionProps.marginBottom ? twipsToPx(sectionProps.marginBottom) : 72;
2770
- const contentWidth = pageWidthPx - marginLeftPx - marginRightPx;
2771
2857
  // Generate column layout CSS if multi-column
2772
2858
  const columnLayoutCss = generateColumnLayoutCss(sectionProps);
2773
2859
  // Check if we have absolutely positioned elements that need a relative container