docgen-utils 1.0.19 → 1.0.21

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