UNPKG

docxml

Version:

TypeScript (component) library for building and parsing a DOCX file

164 lines (158 loc) 5.92 kB
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);