docxml
Version:
TypeScript (component) library for building and parsing a DOCX file
164 lines (158 loc) • 5.92 kB
JavaScript
import { Component } from '../classes/Component.js';
import { tableCellPropertiesToNode, } from '../properties/table-cell-properties.js';
import { checkForForbiddenParameters, isValidNumber } from '../utilities/parameter-checking.js';
import { createChildComponentsFromNodes, registerComponent } from '../utilities/components.js';
import { create } from '../utilities/dom.js';
import { QNS } from '../utilities/namespaces.js';
import { evaluateXPathToMap } from '../utilities/xquery.js';
import { Paragraph } from './Paragraph.js';
import { Row } from './Row.js';
import { Table } from './Table.js';
/**
* A component that represents a table cell.
*
* For MS Word to be happy any cell needs to have a paragraph as the last child. This component will
* quietly fix that for you if you don't have a paragraph there already.
*/
export class Cell extends Component {
constructor(cellProps, ...cellChild) {
// Ensure that properties of type `number` are not `NaN`.
checkForForbiddenParameters(cellProps, isValidNumber, true);
super(cellProps, ...cellChild);
}
/**
* Creates an XML DOM node for this component instance.
*/
async toNode(ancestry) {
const table = ancestry.find((ancestor) => ancestor instanceof Table);
if (!table) {
throw new Error('A cell cannot be rendered outside the context of a table');
}
const children = (await this.childrenToNode(ancestry));
if (!(this.children[this.children.length - 1] instanceof Paragraph)) {
// Cells must always end with a paragraph, or MS Word will complain about
// file corruption.
children.push(await new Paragraph({}).toNode([this, ...ancestry]));
}
return create(`element ${QNS.w}tc {
$tcPr,
$children
}`, {
tcPr: tableCellPropertiesToNode({
colSpan: this.getColSpan(),
rowSpan: this.getRowSpan(),
width: table.props.columnWidths?.[table.model.getCellInfo(this).column] || null,
...this.props,
}, false),
children,
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
toRepeatingNode(ancestry, column, _row) {
const table = ancestry.find((ancestor) => ancestor instanceof Table);
if (!table) {
throw new Error('A cell cannot be rendered outside the context of a table');
}
const info = table.model.getCellInfo(this);
if (column > info.column) {
// Colspans are only recorded on the left-most cell coordinate. No extra node needed;
return null;
}
return create(`element ${QNS.w}tc {
$tcPr,
element ${QNS.w}p {}
}`, {
tcPr: tableCellPropertiesToNode({
width: table.props.columnWidths?.[info.column] || null,
colSpan: this.getColSpan(),
rowSpan: this.getRowSpan(),
...this.props,
}, true),
});
}
/**
* Returns `true` when this cell has no visual representation because a column-spanning or row-
* spanning neighbour overlaps it.
*/
isMergedAway(ancestry) {
const row = ancestry.find((ancestor) => ancestor instanceof Row);
if (!row) {
throw new Error('A cell cannot be rendered outside the context of a row');
}
const table = ancestry.find((ancestor) => ancestor instanceof Table);
if (!table) {
throw new Error('A cell cannot be rendered outside the context of a table');
}
const x = row.children.indexOf(this);
const y = table.children.indexOf(row);
if (y === -1 || x === -1) {
throw new Error('The cell is not part of this table');
}
const info = table.model.getCellInfo(this);
return info.column !== x || info.row !== y;
}
getColSpan() {
return this.props.colSpan || 1;
}
getRowSpan() {
return this.props.rowSpan || 1;
}
/**
* Asserts whether or not a given XML node correlates with this component.
*/
static matchesNode(node) {
return node.nodeName === 'w:tc';
}
/**
* Instantiate this component from the XML in an existing DOCX file.
*/
static fromNode(node, context) {
const { mergedAway, children, ...props } = evaluateXPathToMap(`
let $colStart := docxml:cell-column(.)
let $rowStart := count(../preceding-sibling::${QNS.w}tr)
let $firstNextRow := ../following-sibling::${QNS.w}tr[
child::${QNS.w}tc[docxml:spans-cell-column(., $colStart) and not(
./${QNS.w}tcPr/${QNS.w}vMerge[
@${QNS.w}val = "continue" or
not(./@${QNS.w}val)
]
)]
][1]
let $rowEnd := if ($firstNextRow)
then count($firstNextRow/preceding-sibling::${QNS.w}tr)
else count(../../${QNS.w}tr)
let $mergeCell := boolean(./${QNS.w}tcPr/${QNS.w}vMerge[not(./@${QNS.w}val)])
return map {
"mergedAway": $mergeCell,
"colSpan": if (./${QNS.w}tcPr/${QNS.w}gridSpan)
then ./${QNS.w}tcPr/${QNS.w}gridSpan/@${QNS.w}val/number()
else 1,
"rowSpan": $rowEnd - $rowStart,
"children": array{ ./(${QNS.w}p) },
"verticalAlignment": ./${QNS.w}tcPr/${QNS.w}vAlign/@${QNS.w}val/string()
}
`, node);
if (mergedAway) {
return null;
}
return new Cell(props, ...createChildComponentsFromNodes(this.children, children, context));
}
}
Object.defineProperty(Cell, "children", {
enumerable: true,
configurable: true,
writable: true,
value: [
'Paragraph',
'Table',
'BookmarkRangeStart',
'BookmarkRangeEnd',
]
});
Object.defineProperty(Cell, "mixed", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
registerComponent(Cell);