@yinyoudexing/xml2word 0.1.3 → 0.1.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/dist/index.cjs CHANGED
@@ -50,10 +50,9 @@ ${bodyXml}
50
50
  <w:sectPr>
51
51
  <w:headerReference w:type="default" r:id="rId6"/>
52
52
  <w:footerReference w:type="default" r:id="rId7"/>
53
- <w:pgSz w:w="12240" w:h="15840"/>
53
+ <w:pgSz w:w="11906" w:h="16838"/>
54
54
  <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708" w:footer="708" w:gutter="0"/>
55
55
  <w:cols w:space="708"/>
56
- <w:docGrid w:linePitch="360"/>
57
56
  </w:sectPr>
58
57
  </w:body>
59
58
  </w:document>
@@ -627,6 +626,92 @@ function normalizeFontFamily(value) {
627
626
  if (!first) return void 0;
628
627
  return first.replace(/^["']|["']$/g, "");
629
628
  }
629
+ function cssSelectorsHitTarget(selectorsText, targetSelector) {
630
+ const target = targetSelector.trim().toLowerCase();
631
+ if (!target) return false;
632
+ const selectors = selectorsText.replace(/\/\*[\s\S]*?\*\//g, " ").split(",").map((s) => s.replace(/\s+/g, " ").trim().toLowerCase()).filter(Boolean);
633
+ return selectors.some((s) => {
634
+ if (s === target) return true;
635
+ if (!s.startsWith(target)) return false;
636
+ const rest = s.slice(target.length);
637
+ return rest.startsWith(".") || rest.startsWith(":") || rest.startsWith("#") || rest.startsWith("[");
638
+ });
639
+ }
640
+ function parseCssRuleFromHtml(html, selector) {
641
+ const styleTagRegex = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
642
+ const merged = {};
643
+ let m;
644
+ while (m = styleTagRegex.exec(html)) {
645
+ const cssText = m[1] ?? "";
646
+ const target = selector.trim().toLowerCase();
647
+ if (!target) continue;
648
+ let depth = 0;
649
+ let lastRuleEnd = 0;
650
+ let ruleStart = -1;
651
+ let declStart = -1;
652
+ for (let i = 0; i < cssText.length; i++) {
653
+ const ch = cssText[i];
654
+ if (ch === "{") {
655
+ if (depth === 0) {
656
+ ruleStart = lastRuleEnd;
657
+ declStart = i + 1;
658
+ }
659
+ depth++;
660
+ continue;
661
+ }
662
+ if (ch === "}") {
663
+ if (depth > 0) depth--;
664
+ if (depth === 0 && ruleStart >= 0 && declStart >= 0) {
665
+ const selectorsText = cssText.slice(ruleStart, declStart - 1).trim().toLowerCase();
666
+ const decl = cssText.slice(declStart, i).trim();
667
+ lastRuleEnd = i + 1;
668
+ ruleStart = -1;
669
+ declStart = -1;
670
+ const hit = cssSelectorsHitTarget(selectorsText, target);
671
+ if (hit) Object.assign(merged, parseStyleAttribute(decl));
672
+ }
673
+ }
674
+ }
675
+ }
676
+ return merged;
677
+ }
678
+ function parseCssRuleFromHtmlFirst(html, selector) {
679
+ const styleTagRegex = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
680
+ let m;
681
+ while (m = styleTagRegex.exec(html)) {
682
+ const cssText = m[1] ?? "";
683
+ const target = selector.trim().toLowerCase();
684
+ if (!target) continue;
685
+ let depth = 0;
686
+ let lastRuleEnd = 0;
687
+ let ruleStart = -1;
688
+ let declStart = -1;
689
+ for (let i = 0; i < cssText.length; i++) {
690
+ const ch = cssText[i];
691
+ if (ch === "{") {
692
+ if (depth === 0) {
693
+ ruleStart = lastRuleEnd;
694
+ declStart = i + 1;
695
+ }
696
+ depth++;
697
+ continue;
698
+ }
699
+ if (ch === "}") {
700
+ if (depth > 0) depth--;
701
+ if (depth === 0 && ruleStart >= 0 && declStart >= 0) {
702
+ const selectorsText = cssText.slice(ruleStart, declStart - 1).trim().toLowerCase();
703
+ const decl = cssText.slice(declStart, i).trim();
704
+ lastRuleEnd = i + 1;
705
+ ruleStart = -1;
706
+ declStart = -1;
707
+ const hit = cssSelectorsHitTarget(selectorsText, target);
708
+ if (hit) return parseStyleAttribute(decl);
709
+ }
710
+ }
711
+ }
712
+ }
713
+ return {};
714
+ }
630
715
  function mergeTextStyle(base, patch) {
631
716
  return {
632
717
  bold: patch.bold ?? base.bold,
@@ -903,10 +988,29 @@ function parseCssLengthToTwips(value, baseFontHalfPoints) {
903
988
  const basePt = baseFontHalfPoints / 2;
904
989
  return Math.round(Number(em[1]) * basePt * 20);
905
990
  }
991
+ const rem = v.match(/^(-?\d+(?:\.\d+)?)rem$/);
992
+ if (rem) return Math.round(Number(rem[1]) * 16 * 72 * 20 / 96);
906
993
  const num = v.match(/^(-?\d+(?:\.\d+)?)$/);
907
994
  if (num) return Math.round(Number(num[1]));
908
995
  return void 0;
909
996
  }
997
+ function extractMarginBeforeAfterTwips(css, baseFontHalfPoints) {
998
+ const before = parseCssLengthToTwips(css["margin-top"], baseFontHalfPoints);
999
+ const after = parseCssLengthToTwips(css["margin-bottom"], baseFontHalfPoints);
1000
+ if (typeof before === "number" || typeof after === "number") {
1001
+ return { beforeTwips: before, afterTwips: after };
1002
+ }
1003
+ const m = css.margin?.trim().toLowerCase();
1004
+ if (!m) return {};
1005
+ const tokens = m.split(/\s+/).filter(Boolean);
1006
+ if (!tokens.length) return {};
1007
+ const topToken = tokens[0];
1008
+ const bottomToken = tokens.length === 1 ? tokens[0] : tokens.length === 2 ? tokens[0] : tokens[2] ?? tokens[0];
1009
+ return {
1010
+ beforeTwips: parseCssLengthToTwips(topToken, baseFontHalfPoints),
1011
+ afterTwips: parseCssLengthToTwips(bottomToken, baseFontHalfPoints)
1012
+ };
1013
+ }
910
1014
  function inferFirstFontSizeHalfPoints(node) {
911
1015
  const stack = [node];
912
1016
  while (stack.length) {
@@ -923,8 +1027,10 @@ function inferFirstFontSizeHalfPoints(node) {
923
1027
  }
924
1028
  return void 0;
925
1029
  }
926
- function buildParagraphPrXml(node, baseFontHalfPoints, extraInd, pStyleId) {
927
- const css = parseStyleAttribute(node.attribs?.style);
1030
+ function buildParagraphPrXml(node, baseFontHalfPoints, extraInd, pStyleId, defaultCss) {
1031
+ const tag = node.type === "tag" ? node.name?.toLowerCase() : void 0;
1032
+ const inlineCss = parseStyleAttribute(node.attribs?.style);
1033
+ const css = defaultCss ? { ...defaultCss, ...inlineCss } : inlineCss;
928
1034
  const parts = [];
929
1035
  if (pStyleId) parts.push(`<w:pStyle w:val="${escapeXmlText(pStyleId)}"/>`);
930
1036
  const shdHex = extractBackgroundFillHex(css);
@@ -951,10 +1057,17 @@ function buildParagraphPrXml(node, baseFontHalfPoints, extraInd, pStyleId) {
951
1057
  if (typeof hangingTwips === "number") indAttrs.push(`w:hanging="${hangingTwips}"`);
952
1058
  if (typeof firstLine === "number") indAttrs.push(`w:firstLine="${firstLine}"`);
953
1059
  if (indAttrs.length) parts.push(`<w:ind ${indAttrs.join(" ")}/>`);
954
- const before = parseCssLengthToTwips(css["margin-top"], baseFontHalfPoints);
955
- const after = parseCssLengthToTwips(css["margin-bottom"], baseFontHalfPoints);
1060
+ const hasInlineBefore = inlineCss["margin-top"] != null;
1061
+ const hasInlineAfter = inlineCss["margin-bottom"] != null;
1062
+ const beforeToken = inlineCss["margin-top"] ?? (pStyleId ? void 0 : defaultCss?.["margin-top"]);
1063
+ const afterToken = inlineCss["margin-bottom"] ?? (pStyleId ? void 0 : defaultCss?.["margin-bottom"]);
1064
+ let before = parseCssLengthToTwips(beforeToken, baseFontHalfPoints);
1065
+ let after = parseCssLengthToTwips(afterToken, baseFontHalfPoints);
1066
+ if (tag === "p" && !hasInlineBefore && typeof before === "number") before = Math.min(before, 160);
1067
+ if (tag === "p" && !hasInlineAfter && typeof after === "number") after = Math.min(after, 160);
956
1068
  const lineHeight = (() => {
957
- const lh = css["line-height"]?.trim().toLowerCase();
1069
+ const lhToken = inlineCss["line-height"] ?? (pStyleId ? void 0 : defaultCss?.["line-height"]);
1070
+ const lh = lhToken?.trim().toLowerCase();
958
1071
  if (!lh || lh === "normal") return void 0;
959
1072
  const unitless = lh.match(/^(\d+(?:\.\d+)?)$/);
960
1073
  if (unitless) {
@@ -969,8 +1082,16 @@ function buildParagraphPrXml(node, baseFontHalfPoints, extraInd, pStyleId) {
969
1082
  })();
970
1083
  if (typeof before === "number" || typeof after === "number" || typeof lineHeight === "number") {
971
1084
  const attrs = [];
972
- if (typeof before === "number") attrs.push(`w:before="${Math.max(0, before)}"`);
973
- if (typeof after === "number") attrs.push(`w:after="${Math.max(0, after)}"`);
1085
+ if (typeof before === "number") {
1086
+ attrs.push(`w:before="${Math.max(0, before)}"`);
1087
+ } else if (typeof lineHeight === "number") {
1088
+ attrs.push('w:before="0"');
1089
+ }
1090
+ if (typeof after === "number") {
1091
+ attrs.push(`w:after="${Math.max(0, after)}"`);
1092
+ } else if (typeof lineHeight === "number") {
1093
+ attrs.push('w:after="160"');
1094
+ }
974
1095
  if (typeof lineHeight === "number") {
975
1096
  attrs.push(`w:line="${lineHeight}"`, 'w:lineRule="exact"');
976
1097
  }
@@ -979,10 +1100,13 @@ function buildParagraphPrXml(node, baseFontHalfPoints, extraInd, pStyleId) {
979
1100
  if (!parts.length) return "";
980
1101
  return `<w:pPr>${parts.join("")}</w:pPr>`;
981
1102
  }
982
- function buildParagraphXmlFromContainer(node, baseStyle, extraInd, pStyleId, result) {
983
- const containerStyle = mergeTextStyle(baseStyle, styleFromElement(node));
1103
+ function buildParagraphXmlFromContainer(node, baseStyle, extraInd, pStyleId, result, ctx) {
1104
+ const seededBaseStyle = ctx ? { fontSizeHalfPoints: ctx.defaultBaseFontHalfPoints } : {};
1105
+ const containerStyle = mergeTextStyle(mergeTextStyle(seededBaseStyle, baseStyle), styleFromElement(node));
984
1106
  const baseFontHalfPoints = containerStyle.fontSizeHalfPoints ?? inferFirstFontSizeHalfPoints(node) ?? 28;
985
- const pPrXml = buildParagraphPrXml(node, baseFontHalfPoints, extraInd, pStyleId);
1107
+ const computedBaseFontHalfPoints = containerStyle.fontSizeHalfPoints ?? inferFirstFontSizeHalfPoints(node) ?? ctx?.defaultBaseFontHalfPoints ?? 28;
1108
+ const defaultCss = ctx ? node.type === "tag" && node.name?.toLowerCase() === "p" ? { ...ctx.defaultBodyCss, ...ctx.defaultPCss } : ctx.defaultBodyCss : void 0;
1109
+ const pPrXml = buildParagraphPrXml(node, computedBaseFontHalfPoints, extraInd, pStyleId, defaultCss);
986
1110
  const runs = [];
987
1111
  const res = result ?? {
988
1112
  bodyXml: "",
@@ -1007,6 +1131,11 @@ function buildParagraphXmlFromContainer(node, baseStyle, extraInd, pStyleId, res
1007
1131
  if (!rXml.length) return "";
1008
1132
  return `<w:p>${pPrXml}${rXml.join("")}</w:p>`;
1009
1133
  }
1134
+ function buildSpacerParagraphXml(afterTwips) {
1135
+ const after = Math.max(0, Math.round(afterTwips));
1136
+ if (!after) return "";
1137
+ return `<w:p><w:pPr><w:spacing w:before="0" w:after="${after}" w:line="1" w:lineRule="exact"/></w:pPr><w:r><w:rPr><w:sz w:val="1"/><w:szCs w:val="1"/></w:rPr><w:t></w:t></w:r></w:p>`;
1138
+ }
1010
1139
  function isExplicitPageBreak(node) {
1011
1140
  if (node.type !== "tag") return false;
1012
1141
  const tag = node.name?.toLowerCase();
@@ -1015,13 +1144,14 @@ function isExplicitPageBreak(node) {
1015
1144
  const classList = cls ? cls.split(/\s+/) : [];
1016
1145
  if (tag === "hr" && classList.includes("page-break")) return true;
1017
1146
  if (classList.includes("page-break")) return true;
1147
+ if (classList.includes("umo-page-break")) return true;
1018
1148
  if (node.attribs?.["data-page-break"] === "true") return true;
1019
1149
  const after = css["page-break-after"]?.toLowerCase() ?? css["break-after"]?.toLowerCase();
1020
1150
  const before = css["page-break-before"]?.toLowerCase() ?? css["break-before"]?.toLowerCase();
1021
1151
  if (after?.includes("always") || before?.includes("always")) return true;
1022
1152
  return false;
1023
1153
  }
1024
- function buildListBlocks(listNode, ordered, level, result) {
1154
+ function buildListBlocks(listNode, ordered, level, result, ctx) {
1025
1155
  const liNodes = (listNode.children ?? []).filter(
1026
1156
  (c) => c.type === "tag" && c.name?.toLowerCase() === "li"
1027
1157
  );
@@ -1061,12 +1191,13 @@ function buildListBlocks(listNode, ordered, level, result) {
1061
1191
  rXml.push(buildRunXml(token.style, text));
1062
1192
  }
1063
1193
  if (rXml.length) {
1064
- const baseFontHalfPoints = inferFirstFontSizeHalfPoints(li) ?? 28;
1194
+ const baseFontHalfPoints = inferFirstFontSizeHalfPoints(li) ?? ctx?.defaultBaseFontHalfPoints ?? 28;
1065
1195
  const pPrXml = buildParagraphPrXml(
1066
1196
  li,
1067
1197
  baseFontHalfPoints,
1068
1198
  { leftTwips, hangingTwips },
1069
- void 0
1199
+ void 0,
1200
+ ctx?.defaultBodyCss
1070
1201
  );
1071
1202
  const numPrXml = `<w:numPr><w:ilvl w:val="${ilvl}"/><w:numId w:val="${numId}"/></w:numPr>`;
1072
1203
  const mergedPPrXml = pPrXml ? pPrXml.replace("<w:pPr>", `<w:pPr>${numPrXml}`) : `<w:pPr>${numPrXml}<w:ind w:left="${leftTwips}" w:hanging="${hangingTwips}"/></w:pPr>`;
@@ -1074,7 +1205,7 @@ function buildListBlocks(listNode, ordered, level, result) {
1074
1205
  }
1075
1206
  for (const nested of nestedLists) {
1076
1207
  const nestedOrdered = nested.name?.toLowerCase() === "ol";
1077
- out.push(...buildListBlocks(nested, nestedOrdered, ilvl + 1, result));
1208
+ out.push(...buildListBlocks(nested, nestedOrdered, ilvl + 1, result, ctx));
1078
1209
  }
1079
1210
  }
1080
1211
  return out;
@@ -1153,7 +1284,7 @@ function injectTableCellParagraphSpacing(pXml) {
1153
1284
  }
1154
1285
  return pXml.replace("<w:p>", `<w:p><w:pPr>${spacingXml}</w:pPr>`);
1155
1286
  }
1156
- function buildTableCellBlocksXml(cell, baseStyle, result) {
1287
+ function buildTableCellBlocksXml(cell, baseStyle, result, ctx) {
1157
1288
  const children = cell.children ?? [];
1158
1289
  const hasBlocks = children.some((c) => {
1159
1290
  if (c.type !== "tag") return false;
@@ -1162,7 +1293,7 @@ function buildTableCellBlocksXml(cell, baseStyle, result) {
1162
1293
  });
1163
1294
  const out = [];
1164
1295
  if (!hasBlocks) {
1165
- const p = buildParagraphXmlFromContainer(cell, baseStyle, void 0, void 0, result);
1296
+ const p = buildParagraphXmlFromContainer(cell, baseStyle, void 0, void 0, result, ctx);
1166
1297
  if (p) out.push(p);
1167
1298
  return out.length ? out.map(injectTableCellParagraphSpacing).join("") : "<w:p/>";
1168
1299
  }
@@ -1170,18 +1301,18 @@ function buildTableCellBlocksXml(cell, baseStyle, result) {
1170
1301
  if (c.type === "tag") {
1171
1302
  const tag = c.name?.toLowerCase();
1172
1303
  if (tag === "p") {
1173
- const p = buildParagraphXmlFromContainer(c, baseStyle, void 0, void 0, result);
1304
+ const p = buildParagraphXmlFromContainer(c, baseStyle, void 0, void 0, result, ctx);
1174
1305
  if (p) out.push(p);
1175
1306
  continue;
1176
1307
  }
1177
1308
  if (tag && /^h[1-6]$/.test(tag)) {
1178
1309
  const level = Number(tag.slice(1));
1179
- const p = buildParagraphXmlFromContainer(c, baseStyle, void 0, `Heading${level}`, result);
1310
+ const p = buildParagraphXmlFromContainer(c, baseStyle, void 0, `Heading${level}`, result, ctx);
1180
1311
  if (p) out.push(p);
1181
1312
  continue;
1182
1313
  }
1183
1314
  if (tag === "ul" || tag === "ol") {
1184
- out.push(...buildListBlocks(c, tag === "ol", 0, result));
1315
+ out.push(...buildListBlocks(c, tag === "ol", 0, result, ctx));
1185
1316
  continue;
1186
1317
  }
1187
1318
  if (tag === "img" || tag === "canvas") {
@@ -1194,7 +1325,7 @@ function buildTableCellBlocksXml(cell, baseStyle, result) {
1194
1325
  if (!out.length) return "<w:p/>";
1195
1326
  return out.map(injectTableCellParagraphSpacing).join("");
1196
1327
  }
1197
- function buildTableXml(tableNode, result) {
1328
+ function buildTableXml(tableNode, result, ctx) {
1198
1329
  const rows = [];
1199
1330
  const stack = [...tableNode.children ?? []];
1200
1331
  while (stack.length) {
@@ -1207,8 +1338,18 @@ function buildTableXml(tableNode, result) {
1207
1338
  );
1208
1339
  const colCount = Math.max(0, ...rowCells.map((cells) => cells.length));
1209
1340
  const maxTableWidthTwips = 9360;
1341
+ const colGroup = (tableNode.children ?? []).find((c) => c.type === "tag" && c.name === "colgroup");
1342
+ const colWidthsFromGroup = [];
1343
+ if (colGroup) {
1344
+ const cols = (colGroup.children ?? []).filter((c) => c.type === "tag" && c.name === "col");
1345
+ for (const col of cols) {
1346
+ const css = parseStyleAttribute(col.attribs?.style);
1347
+ const w = parseCssLengthToTwips(css.width ?? css["min-width"], 28);
1348
+ colWidthsFromGroup.push(w);
1349
+ }
1350
+ }
1210
1351
  const estimatedColWidths = new Array(colCount).fill(0).map((_, i) => {
1211
- let explicit;
1352
+ let explicit = colWidthsFromGroup[i];
1212
1353
  let estimated = 0;
1213
1354
  for (const cells of rowCells) {
1214
1355
  const cell = cells[i];
@@ -1221,7 +1362,7 @@ function buildTableXml(tableNode, result) {
1221
1362
  const wTwips = estimateTextWidthTwips(text, baseFontHalfPoints) + 240;
1222
1363
  estimated = Math.max(estimated, wTwips);
1223
1364
  }
1224
- const base = typeof explicit === "number" ? explicit : estimated || Math.round(maxTableWidthTwips / Math.max(1, colCount));
1365
+ const base = typeof explicit === "number" ? Math.max(explicit, estimated) : estimated || Math.round(maxTableWidthTwips / Math.max(1, colCount));
1225
1366
  return Math.max(720, Math.min(6e3, Math.round(base)));
1226
1367
  });
1227
1368
  const normalizedColWidths = (() => {
@@ -1246,7 +1387,7 @@ function buildTableXml(tableNode, result) {
1246
1387
  const cell = cells[i];
1247
1388
  const isHeader = cell.name === "th";
1248
1389
  const baseStyle = isHeader ? { bold: true } : {};
1249
- const paragraphs = buildTableCellBlocksXml(cell, baseStyle, result);
1390
+ const paragraphs = buildTableCellBlocksXml(cell, baseStyle, result, ctx);
1250
1391
  const css = parseStyleAttribute(cell.attribs?.style);
1251
1392
  const widthTwips = parseCellWidthTwips(cell) ?? normalizedColWidths[i];
1252
1393
  const tcW = typeof widthTwips === "number" ? `<w:tcW w:w="${widthTwips}" w:type="dxa"/>` : `<w:tcW w:w="0" w:type="auto"/>`;
@@ -1335,14 +1476,14 @@ function buildTableXml(tableNode, result) {
1335
1476
  const tblPr = `<w:tblPr>${tblW}<w:tblLayout w:type="fixed"/>${tblAlign}${tblBorder}</w:tblPr>`;
1336
1477
  return `<w:tbl>${tblPr}${tblGrid}${rowXml.join("")}</w:tbl>`;
1337
1478
  }
1338
- function buildParagraphXmlFromSingleInlineNode(node, baseStyle, result) {
1479
+ function buildParagraphXmlFromSingleInlineNode(node, baseStyle, result, ctx) {
1339
1480
  const wrapper = {
1340
1481
  type: "tag",
1341
1482
  name: "p",
1342
1483
  attribs: { style: "text-align: center;" },
1343
1484
  children: [node]
1344
1485
  };
1345
- return buildParagraphXmlFromContainer(wrapper, baseStyle, void 0, void 0, result);
1486
+ return buildParagraphXmlFromContainer(wrapper, baseStyle, void 0, void 0, result, ctx);
1346
1487
  }
1347
1488
  function isRecognizedBlockTag(tag) {
1348
1489
  if (!tag) return false;
@@ -1354,7 +1495,22 @@ function isRecognizedBlockTag(tag) {
1354
1495
  if (tag === "pre") return true;
1355
1496
  return false;
1356
1497
  }
1357
- function collectDivBlocks(node, out, result) {
1498
+ function subtreeHasRecognizedBlocks(root) {
1499
+ const stack = [root];
1500
+ while (stack.length) {
1501
+ const cur = stack.pop();
1502
+ if (cur.type === "tag") {
1503
+ if (isExplicitPageBreak(cur)) return true;
1504
+ if (isRecognizedBlockTag(cur.name?.toLowerCase())) return true;
1505
+ }
1506
+ const children = cur.children ?? [];
1507
+ for (let i = children.length - 1; i >= 0; i--) {
1508
+ stack.push(children[i]);
1509
+ }
1510
+ }
1511
+ return false;
1512
+ }
1513
+ function collectDivBlocks(node, out, result, ctx) {
1358
1514
  const parentStyle = node.attribs?.style;
1359
1515
  const inlineBuffer = [];
1360
1516
  const flushInline = () => {
@@ -1364,7 +1520,7 @@ function collectDivBlocks(node, out, result) {
1364
1520
  attribs: { style: parentStyle },
1365
1521
  children: inlineBuffer.splice(0)
1366
1522
  };
1367
- const pXml = buildParagraphXmlFromContainer(wrapper, {}, void 0, void 0, result);
1523
+ const pXml = buildParagraphXmlFromContainer(wrapper, {}, void 0, void 0, result, ctx);
1368
1524
  if (pXml) out.push(pXml);
1369
1525
  };
1370
1526
  const children = node.children ?? [];
@@ -1378,19 +1534,15 @@ function collectDivBlocks(node, out, result) {
1378
1534
  }
1379
1535
  if (isRecognizedBlockTag(tag)) {
1380
1536
  if (inlineBuffer.length) flushInline();
1381
- collectBodyBlocks(child, out, result);
1537
+ collectBodyBlocks(child, out, result, ctx);
1538
+ continue;
1539
+ }
1540
+ if (subtreeHasRecognizedBlocks(child)) {
1541
+ if (inlineBuffer.length) flushInline();
1542
+ collectBodyBlocks(child, out, result, ctx);
1382
1543
  continue;
1383
1544
  }
1384
1545
  if (tag === "div") {
1385
- const childHasRecognizedBlocks = (child.children ?? []).some((gc) => {
1386
- if (gc.type !== "tag") return false;
1387
- return isRecognizedBlockTag(gc.name?.toLowerCase());
1388
- });
1389
- if (childHasRecognizedBlocks) {
1390
- if (inlineBuffer.length) flushInline();
1391
- collectBodyBlocks(child, out, result);
1392
- continue;
1393
- }
1394
1546
  if (inlineBuffer.length) flushInline();
1395
1547
  const mergedStyle = [parentStyle, child.attribs?.style].filter(Boolean).join(";");
1396
1548
  const wrapper = {
@@ -1399,7 +1551,7 @@ function collectDivBlocks(node, out, result) {
1399
1551
  attribs: { style: mergedStyle || void 0 },
1400
1552
  children: child.children ?? []
1401
1553
  };
1402
- const pXml = buildParagraphXmlFromContainer(wrapper, {}, void 0, void 0, result);
1554
+ const pXml = buildParagraphXmlFromContainer(wrapper, {}, void 0, void 0, result, ctx);
1403
1555
  if (pXml) out.push(pXml);
1404
1556
  continue;
1405
1557
  }
@@ -1408,7 +1560,7 @@ function collectDivBlocks(node, out, result) {
1408
1560
  }
1409
1561
  if (inlineBuffer.length) flushInline();
1410
1562
  }
1411
- function collectBodyBlocks(node, out, result) {
1563
+ function collectBodyBlocks(node, out, result, ctx) {
1412
1564
  if (isSkippableSubtree(node)) return;
1413
1565
  if (node.type === "tag") {
1414
1566
  const tag = node.name?.toLowerCase();
@@ -1417,36 +1569,51 @@ function collectBodyBlocks(node, out, result) {
1417
1569
  return;
1418
1570
  }
1419
1571
  if (tag === "p") {
1420
- const pXml = buildParagraphXmlFromContainer(node, {}, void 0, void 0, result);
1572
+ const pXml = buildParagraphXmlFromContainer(node, {}, void 0, void 0, result, ctx);
1421
1573
  if (pXml) out.push(pXml);
1422
1574
  return;
1423
1575
  }
1424
1576
  if (tag === "img" || tag === "canvas") {
1425
- const pXml = buildParagraphXmlFromSingleInlineNode(node, {}, result);
1577
+ const pXml = buildParagraphXmlFromSingleInlineNode(node, {}, result, ctx);
1426
1578
  if (pXml) out.push(pXml);
1427
1579
  return;
1428
1580
  }
1429
1581
  if (tag && /^h[1-6]$/.test(tag)) {
1430
1582
  const level = Number(tag.slice(1));
1431
- const hXml = buildParagraphXmlFromContainer(node, {}, void 0, `Heading${level}`, result);
1583
+ const hXml = buildParagraphXmlFromContainer(node, {}, void 0, `Heading${level}`, result, ctx);
1432
1584
  if (hXml) out.push(hXml);
1433
1585
  return;
1434
1586
  }
1435
1587
  if (tag === "table") {
1436
- const tblXml = buildTableXml(node, result);
1588
+ const tblXml = buildTableXml(node, result, ctx);
1437
1589
  if (tblXml) out.push(tblXml);
1438
1590
  return;
1439
1591
  }
1440
1592
  if (tag === "ul" || tag === "ol") {
1441
- out.push(...buildListBlocks(node, tag === "ol", 0, result));
1593
+ out.push(...buildListBlocks(node, tag === "ol", 0, result, ctx));
1442
1594
  return;
1443
1595
  }
1444
1596
  if (tag === "div") {
1445
- collectDivBlocks(node, out, result);
1597
+ if (hasClass(node, "tableWrapper")) {
1598
+ const display = ctx.tableWrapperCss.display?.trim().toLowerCase();
1599
+ if (display !== "contents") {
1600
+ const baseFontHalfPoints = inferFirstFontSizeHalfPoints(node) ?? ctx.defaultBaseFontHalfPoints;
1601
+ const { beforeTwips, afterTwips } = extractMarginBeforeAfterTwips(ctx.tableWrapperCss, baseFontHalfPoints);
1602
+ const beforeXml = typeof beforeTwips === "number" && beforeTwips > 0 ? buildSpacerParagraphXml(beforeTwips) : "";
1603
+ const afterXml = typeof afterTwips === "number" && afterTwips > 0 ? buildSpacerParagraphXml(afterTwips) : "";
1604
+ if (beforeXml) out.push(beforeXml);
1605
+ collectDivBlocks(node, out, result, ctx);
1606
+ if (afterXml) out.push(afterXml);
1607
+ return;
1608
+ }
1609
+ collectDivBlocks(node, out, result, ctx);
1610
+ return;
1611
+ }
1612
+ collectDivBlocks(node, out, result, ctx);
1446
1613
  return;
1447
1614
  }
1448
1615
  }
1449
- for (const c of node.children ?? []) collectBodyBlocks(c, out, result);
1616
+ for (const c of node.children ?? []) collectBodyBlocks(c, out, result, ctx);
1450
1617
  }
1451
1618
  function textToWordBodyXml(text) {
1452
1619
  const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
@@ -1466,6 +1633,20 @@ function textToWordBodyXml(text) {
1466
1633
  }
1467
1634
  function htmlToWordBody(html) {
1468
1635
  const normalized = html.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1636
+ const bodyCss = parseCssRuleFromHtmlFirst(normalized, "body");
1637
+ const pCss = parseCssRuleFromHtmlFirst(normalized, "p");
1638
+ const defaultBaseFontHalfPoints = parseFontSizeToHalfPoints(bodyCss["font-size"]) ?? 28;
1639
+ const defaultBodyCss = {};
1640
+ if (bodyCss["line-height"]) defaultBodyCss["line-height"] = bodyCss["line-height"];
1641
+ const defaultPCss = {};
1642
+ if (pCss["line-height"]) defaultPCss["line-height"] = pCss["line-height"];
1643
+ if (pCss["margin-top"]) defaultPCss["margin-top"] = pCss["margin-top"];
1644
+ if (pCss["margin-bottom"]) defaultPCss["margin-bottom"] = pCss["margin-bottom"];
1645
+ if (pCss["text-align"]) defaultPCss["text-align"] = pCss["text-align"];
1646
+ const tableWrapperCss = {
1647
+ ...parseCssRuleFromHtml(normalized, ".tableWrapper"),
1648
+ ...parseCssRuleFromHtml(normalized, ".tiptap .tableWrapper")
1649
+ };
1469
1650
  const doc = (0, import_htmlparser2.parseDocument)(normalized, {
1470
1651
  lowerCaseAttributeNames: true,
1471
1652
  lowerCaseTags: true,
@@ -1473,7 +1654,8 @@ function htmlToWordBody(html) {
1473
1654
  });
1474
1655
  const result = { bodyXml: "", images: [] };
1475
1656
  const out = [];
1476
- collectBodyBlocks(doc, out, result);
1657
+ const ctx = { defaultBaseFontHalfPoints, defaultBodyCss, defaultPCss, tableWrapperCss };
1658
+ collectBodyBlocks(doc, out, result, ctx);
1477
1659
  result.bodyXml = out.join("");
1478
1660
  return result;
1479
1661
  }