Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/components/document/src/Cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,20 @@ export class Cell extends Component<CellProps, CellChild> {
"insideH": docxml:ct-border(${QNS.w}insideH),
"insideV": docxml:ct-border(${QNS.w}insideV)
},
"margin": ./${QNS.w}tcPr/${QNS.w}tcMar/map {
"top": if (${QNS.w}top/@${QNS.w}w)
then docxml:length(${QNS.w}top/@${QNS.w}w, 'twip')
else (),
"start": if ((${QNS.w}start|${QNS.w}left)/@${QNS.w}w)
then docxml:length((${QNS.w}start|${QNS.w}left)[1]/@${QNS.w}w, 'twip')
else (),
"bottom": if (${QNS.w}bottom/@${QNS.w}w)
then docxml:length(${QNS.w}bottom/@${QNS.w}w, 'twip')
else (),
"end": if ((${QNS.w}end|${QNS.w}right)/@${QNS.w}w)
then docxml:length((${QNS.w}end|${QNS.w}right)[1]/@${QNS.w}w, 'twip')
else ()
},
"verticalAlignment": ./${QNS.w}tcPr/${QNS.w}vAlign/@${QNS.w}val/string(),
"insertion": ./${QNS.w}tcPr/${QNS.w}cellIns/map {
"id": @${QNS.w}id/number(),
Expand Down
53 changes: 53 additions & 0 deletions lib/components/document/test/Cell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,59 @@ describe('Cell', () => {
});
});

describe('Cell - tcMar (per-cell margins)', () => {
const dom = create(`<w:tbl xmlns:w="${NamespaceUri.w}">
<w:tr>
<w:tc xid="modern">
<w:tcPr>
<w:tcMar>
<w:top w:w="28" w:type="dxa"/>
<w:start w:w="14" w:type="dxa"/>
<w:bottom w:w="28" w:type="dxa"/>
<w:end w:w="14" w:type="dxa"/>
</w:tcMar>
</w:tcPr>
<w:p/>
</w:tc>
<w:tc xid="legacy">
<w:tcPr>
<w:tcMar>
<w:left w:w="14" w:type="dxa"/>
<w:right w:w="14" w:type="dxa"/>
</w:tcMar>
</w:tcPr>
<w:p/>
</w:tc>
</w:tr>
</w:tbl>`);

describe('modern start/end', () => {
const cell = Cell.fromNode(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
evaluateXPathToFirstNode('.//*[@xid="modern"]', dom)!,
emptyContext
);
it('parses all four sides', () => {
expect(cell?.props.margin?.top?.twip).toBe(28);
expect(cell?.props.margin?.start?.twip).toBe(14);
expect(cell?.props.margin?.bottom?.twip).toBe(28);
expect(cell?.props.margin?.end?.twip).toBe(14);
});
});

describe('legacy left/right', () => {
const cell = Cell.fromNode(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
evaluateXPathToFirstNode('.//*[@xid="legacy"]', dom)!,
emptyContext
);
it('maps left/right onto start/end', () => {
expect(cell?.props.margin?.start?.twip).toBe(14);
expect(cell?.props.margin?.end?.twip).toBe(14);
});
});
});

describe('Cell - with colspan', () => {
const tableNode = create(`
<w:tbl xmlns:w="${NamespaceUri.w}">
Expand Down
79 changes: 76 additions & 3 deletions lib/properties/src/table-cell-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ export type TableCellProperties = {
*/
tr2bl?: null | Border<LineBorderType>;
};
/**
* The margins (internal padding) of this cell, overriding the table-level
* default cell margins (`w:tblCellMar`). Corresponds to `w:tcMar` in OOXML.
*
* Each side is a {@link Length}. Omitted sides fall back to the table default.
*/
margin?: null | {
top?: null | Length;
/**
* The leading (left, in left-to-right) margin. Serialized as `w:start`;
* both `w:start` and the legacy `w:left` are accepted when reading.
*/
start?: null | Length;
bottom?: null | Length;
/**
* The trailing (right, in left-to-right) margin. Serialized as `w:end`;
* both `w:end` and the legacy `w:right` are accepted when reading.
*/
end?: null | Length;
};
/**
* The vertical alignment of this cell.
*/
Expand Down Expand Up @@ -127,6 +147,20 @@ export function tableCellPropertiesFromNode(
"insideH": docxml:ct-border(${QNS.w}insideH),
"insideV": docxml:ct-border(${QNS.w}insideV)
},
"margin": ./${QNS.w}tcMar/map {
"top": if (${QNS.w}top/@${QNS.w}w)
then docxml:length(${QNS.w}top/@${QNS.w}w, 'twip')
else (),
"start": if ((${QNS.w}start|${QNS.w}left)/@${QNS.w}w)
then docxml:length((${QNS.w}start|${QNS.w}left)[1]/@${QNS.w}w, 'twip')
else (),
"bottom": if (${QNS.w}bottom/@${QNS.w}w)
then docxml:length(${QNS.w}bottom/@${QNS.w}w, 'twip')
else (),
"end": if ((${QNS.w}end|${QNS.w}right)/@${QNS.w}w)
then docxml:length((${QNS.w}end|${QNS.w}right)[1]/@${QNS.w}w, 'twip')
else ()
},
"verticalAlignment": ./${QNS.w}vAlign/@${QNS.w}val/string(),
"insertion": ./${QNS.w}cellIns/map {
"id": @${QNS.w}id/number(),
Expand All @@ -147,7 +181,7 @@ export function tableCellPropertiesFromNode(
}
`,
node
)
)
: {};

if (props.change) {
Expand Down Expand Up @@ -218,6 +252,24 @@ export function tableCellPropertiesToNode(
docxml:ct-border(fn:QName("${NamespaceUri.w}", "insideH"), $borders('insideH')),
docxml:ct-border(fn:QName("${NamespaceUri.w}", "insideV"), $borders('insideV'))
} else (),
if (exists($margin)) then element ${QNS.w}tcMar {
if (exists($margin('top'))) then element ${QNS.w}top {
attribute ${QNS.w}w { $margin('top') },
attribute ${QNS.w}type { "dxa" }
} else (),
if (exists($margin('start'))) then element ${QNS.w}start {
attribute ${QNS.w}w { $margin('start') },
attribute ${QNS.w}type { "dxa" }
} else (),
if (exists($margin('bottom'))) then element ${QNS.w}bottom {
attribute ${QNS.w}w { $margin('bottom') },
attribute ${QNS.w}type { "dxa" }
} else (),
if (exists($margin('end'))) then element ${QNS.w}end {
attribute ${QNS.w}w { $margin('end') },
attribute ${QNS.w}type { "dxa" }
} else ()
} else (),
if (exists($verticalAlignment)) then element ${QNS.w}vAlign {
attribute ${QNS.w}val { $verticalAlignment }
} else (),
Expand Down Expand Up @@ -247,8 +299,29 @@ export function tableCellPropertiesToNode(
tl2br: null,
tr2bl: null,
...tcpr.borders,
}
}
: null,
margin:
tcpr.margin &&
(tcpr.margin.top ||
tcpr.margin.start ||
tcpr.margin.bottom ||
tcpr.margin.end)
? {
top: tcpr.margin.top
? Math.round(tcpr.margin.top.twip)
: null,
start: tcpr.margin.start
? Math.round(tcpr.margin.start.twip)
: null,
bottom: tcpr.margin.bottom
? Math.round(tcpr.margin.bottom.twip)
: null,
end: tcpr.margin.end
? Math.round(tcpr.margin.end.twip)
: null,
}
: null,
verticalAlignment: tcpr.verticalAlignment || null,
change: tcpr.change
? {
Expand All @@ -260,7 +333,7 @@ export function tableCellPropertiesToNode(
? new Date(tcpr.change.date).toISOString()
: undefined,
node: tableCellPropertiesToNode(tcpr.change, true),
}
}
: null,
insertion: tcpr.insertion
? new CellInsertion(tcpr.insertion).toNode()
Expand Down
40 changes: 40 additions & 0 deletions lib/properties/test/table-cell-properties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,46 @@ describe('Table cell formatting', () => {
});
});

describe('Table cell margins (w:tcMar)', () => {
test(
`<w:tcPr ${ALL_NAMESPACE_DECLARATIONS}>
<w:tcMar>
<w:top w:w="28" w:type="dxa"/>
<w:start w:w="14" w:type="dxa"/>
<w:bottom w:w="28" w:type="dxa"/>
<w:end w:w="14" w:type="dxa"/>
</w:tcMar>
</w:tcPr>`,
{
margin: {
top: twip(28),
start: twip(14),
bottom: twip(28),
end: twip(14),
},
}
);

describe('Legacy "left"/"right"', () => {
test(
`<w:tcPr ${ALL_NAMESPACE_DECLARATIONS}>
<w:tcMar>
<w:left w:w="14" w:type="dxa"/>
<w:right w:w="14" w:type="dxa"/>
</w:tcMar>
</w:tcPr>`,
{
margin: {
top: null,
start: twip(14),
bottom: null,
end: twip(14),
},
}
);
});
});

describe('Table cell insertion', () => {
const date = new Date();
test(
Expand Down