diff --git a/src/lib/export/docx.ts b/src/lib/export/docx.ts
index 80c14a1..9b497f6 100644
--- a/src/lib/export/docx.ts
+++ b/src/lib/export/docx.ts
@@ -1137,7 +1137,8 @@ function textBoxDrawingXml(box: TextBoxDocx, index: number, parts: TxbxParts): s
: ``;
const align = box.wrap === 'right' ? 'right' : 'left';
const emu = (cm: number) => Math.round(cm * 360000);
- const posH = box.offsetCm != null && box.wrap !== 'topBottom'
+ // The x rides topBottom too: it moves the frame within its full-width band.
+ const posH = box.offsetCm != null
? `${emu(box.offsetCm)}`
: `${align}`;
return (
diff --git a/src/lib/import/docx.ts b/src/lib/import/docx.ts
index efd7341..eadaba1 100644
--- a/src/lib/import/docx.ts
+++ b/src/lib/import/docx.ts
@@ -201,18 +201,25 @@ export function importDocx(bytes: Uint8Array, convertedImages: ConvertedImages =
const { groups, midSectPrs } = splitBodySections(Array.from(body.children));
const finalSectPr = fc(body, 'sectPr');
const blocks: Node[] = [];
+ const groupCols = groups.map((g) => sectPrColumns(g.sectPr ?? finalSectPr, ctx));
+ // A continuous break where the column setup changes only encodes a columns region
+ // (the export splits there without opening a section): no section break, and the
+ // group shares the previous one's header/footer set.
+ const colsOnly = groups.map((g, gi) => gi > 0
+ && !sectionStartsNewPage(g.sectPr ?? finalSectPr)
+ && JSON.stringify(groupCols[gi]) !== JSON.stringify(groupCols[gi - 1]));
groups.forEach((g, gi) => {
const inner = convertBlocks(g.els, ctx, 'body');
// A section's own w:type says how it begins: a page-starting break (nextPage/odd/even,
// or the default) puts its first block on a new page; continuous/nextColumn flow on.
- if (gi > 0 && inner.length) {
+ if (gi > 0 && !colsOnly[gi] && inner.length) {
const first = inner[0];
if (first.type === 'paragraph' || first.type === 'heading') {
first.attrs = { ...(first.attrs ?? {}), sectionBreak: true };
if (sectionStartsNewPage(g.sectPr ?? finalSectPr)) first.attrs.breakBefore = 'page';
}
}
- const cols = sectPrColumns(g.sectPr ?? finalSectPr, ctx);
+ const cols = groupCols[gi];
if (cols) pushColumnRuns(inner, cols, blocks, ctx);
else blocks.push(...inner);
});
@@ -239,7 +246,8 @@ export function importDocx(bytes: Uint8Array, convertedImages: ConvertedImages =
// The paper is the *first* section's, as the margins are: a document whose last
// section is one landscape page is not a landscape document.
const docPaper = sectPaper(groups[0]?.sectPr ?? finalSectPr);
- const hfSections = sectionHfSets(groups.map((g) => g.sectPr ?? finalSectPr), ctx, oddEven, docPaper);
+ const hfSections = sectionHfSets(
+ groups.filter((_, gi) => !colsOnly[gi]).map((g) => g.sectPr ?? finalSectPr), ctx, oddEven, docPaper);
const first = hfSections[0];
// A first-page/even zone reserves the header/footer band even when its default is empty;
// the distance is document-wide, so any section having one is enough.
@@ -2215,6 +2223,7 @@ function convertTable(tbl: Element, ctx: Ctx): Node | null {
// A w:tblCellMar / w:tcMar's four sides in cm, taking the first element that declares
// each. w:type="nil" is an explicit zero; a side nobody declares comes back null.
+// Hundredth-cm: a twip is 0.0018cm, so an authored round2 value reads back exactly.
function cellMarginsCm(els: (Element | null)[]): (number | null)[] {
return [['top'], ['right', 'end'], ['bottom'], ['left', 'start']].map((names) => {
for (const el of els) {
@@ -2223,7 +2232,7 @@ function cellMarginsCm(els: (Element | null)[]): (number | null)[] {
if (!s) continue;
if (s.getAttributeNS(W, 'type') === 'nil') return 0;
const w = intAttr(s, W, 'w');
- if (w != null) return twipToCm(w);
+ if (w != null) return round2(twipToCm(w));
}
}
return null;
@@ -2371,7 +2380,7 @@ function docxTableLook(tblPr: Element | null): string | null {
// w:tblW) its width — the rest becomes the editor's right margin. Under the older
// compatibility mode the indent is measured to the cell's *text*, so the table hangs its
// left cell margin into the page margin (tblIndIsToText).
-function tableMargins(tbl: Element, weights: number[] | null, ctx: Ctx, leftPadCm: number): { marginLeft: number; marginRight: number } | null {
+function tableMargins(tbl: Element, weights: number[] | null, ctx: Ctx, leftPadCm: number): { marginLeft?: number; marginRight?: number } | null {
const tblPr = fc(tbl, 'tblPr');
const content = ctx.contentWidthCm;
const dxa = (el: Element | null) => (el?.getAttributeNS(W, 'type') ?? 'dxa') === 'dxa' ? intAttr(el, W, 'w') : null;
@@ -2385,7 +2394,9 @@ function tableMargins(tbl: Element, weights: number[] | null, ctx: Ctx, leftPadC
if (Math.abs(left) < 0.05 && Math.abs(right) < 0.05) return null;
if (left + right > content - 1) return null;
const round2 = (v: number) => Math.round(v * 100) / 100;
- return { marginLeft: round2(left), marginRight: round2(right) };
+ const l = round2(left), r = round2(right);
+ // A zero side is the attr's default (null); rounding may also leave a -0 behind.
+ return { ...(Math.abs(l) >= 0.005 ? { marginLeft: l } : {}), ...(Math.abs(r) >= 0.005 ? { marginRight: r } : {}) };
}
function flattenTable(tbl: Element, ctx: Ctx): Node[] {