@docx4j/core-ts 0.3.2 → 0.4.0

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.
Files changed (34) hide show
  1. package/README.md +3 -1
  2. package/dist/html/convert.d.mts +116 -0
  3. package/dist/html/convert.mjs +541 -0
  4. package/dist/html/elements.d.mts +19 -0
  5. package/dist/html/elements.mjs +182 -0
  6. package/dist/html/index.d.mts +2 -0
  7. package/dist/html/index.mjs +4 -0
  8. package/dist/model/content/Body.mjs +7 -0
  9. package/dist/model/content/InlinePicture.d.mts +18 -3
  10. package/dist/model/content/InlinePicture.mjs +44 -7
  11. package/dist/model/content/Table.d.mts +38 -0
  12. package/dist/model/content/Table.mjs +80 -2
  13. package/dist/model/content/index.d.mts +2 -1
  14. package/dist/model/content/index.mjs +2 -1
  15. package/dist/model/content/stylesFacade.d.mts +4 -2
  16. package/dist/model/content/stylesFacade.mjs +20 -6
  17. package/dist/model/content/tableColumns.d.mts +48 -0
  18. package/dist/model/content/tableColumns.mjs +484 -0
  19. package/dist/model/customxml/CustomXmlPartCollection.d.mts +6 -1
  20. package/dist/model/customxml/CustomXmlPartCollection.mjs +19 -3
  21. package/dist/model/customxml/bindings.d.mts +5 -2
  22. package/dist/model/customxml/bindings.mjs +21 -6
  23. package/dist/model/customxml/index.d.mts +3 -0
  24. package/dist/model/customxml/index.mjs +3 -0
  25. package/dist/model/customxml/opendope.d.mts +32 -0
  26. package/dist/model/customxml/opendope.mjs +56 -0
  27. package/dist/model/customxml/pictures.d.mts +54 -0
  28. package/dist/model/customxml/pictures.mjs +186 -0
  29. package/dist/model/customxml/xhtml.d.mts +42 -0
  30. package/dist/model/customxml/xhtml.mjs +162 -0
  31. package/dist/packages/WordprocessingMLPackage.mjs +1 -0
  32. package/dist/parts/wml/knownStyles.d.mts +1 -0
  33. package/dist/parts/wml/knownStyles.mjs +2233 -0
  34. package/package.json +6 -1
@@ -0,0 +1,19 @@
1
+ import type { PasteBlock } from './convert.mjs';
2
+ /** Word's character style for a link's text, as `Range.hyperlink` gives it. */
3
+ export declare const HYPERLINK_STYLE = "Hyperlink";
4
+ /** What the blocks need from the document before they are built: a definition per list key, a relationship per link address. */
5
+ export interface BuildNeeds {
6
+ lists: {
7
+ key: string;
8
+ bullet: boolean;
9
+ }[];
10
+ links: string[];
11
+ }
12
+ /** The document's answers: `w:numId` per list key, `r:id` per address; undefined drops the list or the link (its text kept). */
13
+ export interface BuildResolvers {
14
+ numId?: (key: string) => string | undefined;
15
+ relId?: (address: string) => string | undefined;
16
+ }
17
+ export declare function needsOf(blocks: readonly PasteBlock[]): BuildNeeds;
18
+ /** The blocks as a WordprocessingML fragment: `w:p` and `w:tbl` elements, the prefixes undeclared (`insertXml` declares them). */
19
+ export declare function blocksToXml(blocks: readonly PasteBlock[], resolve?: BuildResolvers): string;
@@ -0,0 +1,182 @@
1
+ import { highlightNameOf } from './convert.mjs';
2
+ /** Word's character style for a link's text, as `Range.hyperlink` gives it. */
3
+ export const HYPERLINK_STYLE = 'Hyperlink';
4
+ function walkBlocks(blocks, onParagraph) {
5
+ for (const b of blocks) {
6
+ if (b.kind === 'paragraph')
7
+ onParagraph(b);
8
+ else
9
+ for (const row of b.rows)
10
+ for (const c of row)
11
+ walkBlocks(c.blocks, onParagraph);
12
+ }
13
+ }
14
+ export function needsOf(blocks) {
15
+ const lists = new Map();
16
+ const links = new Set();
17
+ walkBlocks(blocks, (p) => {
18
+ if (p.list && !lists.has(p.list.key))
19
+ lists.set(p.list.key, p.list.bullet);
20
+ for (const i of p.inlines)
21
+ if ('text' in i && i.link !== undefined)
22
+ links.add(i.link);
23
+ });
24
+ return { lists: [...lists].map(([key, bullet]) => ({ key, bullet })), links: [...links] };
25
+ }
26
+ const esc = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
27
+ const attr = (value) => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
28
+ function rPrXml(run, linked) {
29
+ const out = [];
30
+ // In the schema's order: rStyle, rFonts, b, i, strike, color, sz, szCs, highlight, u, vertAlign.
31
+ if (linked)
32
+ out.push(`<w:rStyle w:val="${HYPERLINK_STYLE}"/>`);
33
+ else if (run.style)
34
+ out.push(`<w:rStyle w:val="${attr(run.style)}"/>`);
35
+ if (run.fontName)
36
+ out.push(`<w:rFonts w:ascii="${attr(run.fontName)}" w:hAnsi="${attr(run.fontName)}" w:cs="${attr(run.fontName)}"/>`);
37
+ if (run.bold)
38
+ out.push('<w:b/>');
39
+ if (run.italic)
40
+ out.push('<w:i/>');
41
+ if (run.strike)
42
+ out.push('<w:strike/>');
43
+ // A link's colour is its style's, as `Range.hyperlink` leaves no direct colour on a link.
44
+ if (run.color && !linked)
45
+ out.push(`<w:color w:val="${attr(run.color === 'auto' ? 'auto' : run.color.replace(/^#/, ''))}"/>`);
46
+ if (run.fontSize !== undefined) {
47
+ const half = Math.round(run.fontSize * 2);
48
+ out.push(`<w:sz w:val="${half}"/><w:szCs w:val="${half}"/>`);
49
+ }
50
+ if (run.highlight) {
51
+ const name = highlightNameOf(run.highlight);
52
+ if (name)
53
+ out.push(`<w:highlight w:val="${name}"/>`);
54
+ }
55
+ if (run.underline)
56
+ out.push('<w:u w:val="single"/>');
57
+ if (run.vertAlign)
58
+ out.push(`<w:vertAlign w:val="${run.vertAlign}"/>`);
59
+ return out.length > 0 ? `<w:rPr>${out.join('')}</w:rPr>` : '';
60
+ }
61
+ function runXml(run, linked) {
62
+ if (run.text === '')
63
+ return '';
64
+ const space = /^\s|\s$|\s\s/.test(run.text) ? ' xml:space="preserve"' : '';
65
+ return `<w:r>${rPrXml(run, linked)}<w:t${space}>${esc(run.text)}</w:t></w:r>`;
66
+ }
67
+ function inlinesXml(inlines, resolve) {
68
+ var _a;
69
+ const out = [];
70
+ let i = 0;
71
+ while (i < inlines.length) {
72
+ const inline = inlines[i];
73
+ if ('br' in inline) {
74
+ out.push('<w:r><w:br/></w:r>');
75
+ i++;
76
+ continue;
77
+ }
78
+ if ('tab' in inline) {
79
+ out.push('<w:r><w:tab/></w:r>');
80
+ i++;
81
+ continue;
82
+ }
83
+ const rId = inline.link !== undefined ? (_a = resolve.relId) === null || _a === void 0 ? void 0 : _a.call(resolve, inline.link) : undefined;
84
+ if (rId === undefined) {
85
+ out.push(runXml(inline, false));
86
+ i++;
87
+ continue;
88
+ }
89
+ // The runs of one address under one w:hyperlink, in Word's look.
90
+ const runs = [];
91
+ while (i < inlines.length) {
92
+ const next = inlines[i];
93
+ if (!('text' in next) || next.link !== inline.link)
94
+ break;
95
+ runs.push(runXml(next, true));
96
+ i++;
97
+ }
98
+ out.push(`<w:hyperlink r:id="${attr(rId)}">${runs.join('')}</w:hyperlink>`);
99
+ }
100
+ return out.join('');
101
+ }
102
+ function pPrXml(p, resolve) {
103
+ var _a;
104
+ const out = [];
105
+ // In the schema's order: pStyle, numPr, spacing, ind, jc.
106
+ if (p.style)
107
+ out.push(`<w:pStyle w:val="${attr(p.style)}"/>`);
108
+ const numId = p.list ? (_a = resolve.numId) === null || _a === void 0 ? void 0 : _a.call(resolve, p.list.key) : undefined;
109
+ if (p.list && numId !== undefined)
110
+ out.push(`<w:numPr><w:ilvl w:val="${p.list.level}"/><w:numId w:val="${attr(numId)}"/></w:numPr>`);
111
+ if (p.spacing)
112
+ out.push(`<w:spacing w:before="${p.spacing.before}"/>`);
113
+ if (p.ind) {
114
+ const a = [];
115
+ if (p.ind.start !== undefined)
116
+ a.push(`w:left="${p.ind.start}"`);
117
+ if (p.ind.end !== undefined)
118
+ a.push(`w:right="${p.ind.end}"`);
119
+ if (p.ind.hanging !== undefined)
120
+ a.push(`w:hanging="${p.ind.hanging}"`);
121
+ else if (p.ind.firstLine !== undefined)
122
+ a.push(`w:firstLine="${p.ind.firstLine}"`);
123
+ if (a.length > 0)
124
+ out.push(`<w:ind ${a.join(' ')}/>`);
125
+ }
126
+ if (p.jc)
127
+ out.push(`<w:jc w:val="${attr(p.jc)}"/>`);
128
+ return out.length > 0 ? `<w:pPr>${out.join('')}</w:pPr>` : '';
129
+ }
130
+ function paragraphXml(p, resolve) {
131
+ return `<w:p>${pPrXml(p, resolve)}${inlinesXml(p.inlines, resolve)}</w:p>`;
132
+ }
133
+ /** Word's default text width shared equally where the table does not say (A4 or Letter with Word's margins: 9,026 twips). */
134
+ const TEXT_WIDTH = 9026;
135
+ function tableXml(t, resolve) {
136
+ const columns = Math.max(1, ...t.rows.map((r) => r.reduce((n, c) => n + c.colspan, 0)));
137
+ const widths = Array.from({ length: columns }, () => Math.floor(TEXT_WIDTH / columns));
138
+ const first = t.rows[0];
139
+ if (first && first.every((c) => c.width !== undefined && c.colspan === 1) && first.length === columns)
140
+ first.forEach((c, i) => { widths[i] = c.width; });
141
+ const widthOf = (col, span) => { var _a; let w = 0; for (let k = 0; k < span; k++)
142
+ w += (_a = widths[col + k]) !== null && _a !== void 0 ? _a : widths[0]; return w; };
143
+ const tcPr = (col, span, merge) => `<w:tcPr><w:tcW w:w="${widthOf(col, span)}" w:type="dxa"/>${span > 1 ? `<w:gridSpan w:val="${span}"/>` : ''}${merge === 'restart' ? '<w:vMerge w:val="restart"/>' : merge === 'continue' ? '<w:vMerge/>' : ''}</w:tcPr>`;
144
+ const cellXml = (c, col) => {
145
+ const blocks = c.blocks.length > 0 ? blocksToXml(c.blocks, resolve) : '<w:p/>';
146
+ return `<w:tc>${tcPr(col, c.colspan, c.rowspan > 1 ? 'restart' : undefined)}${blocks}</w:tc>`;
147
+ };
148
+ // A cell spanning rows holds its place in the rows below with a continuation cell, as WordprocessingML writes a vertical merge.
149
+ const pending = new Map();
150
+ const rows = t.rows.map((cells, r) => {
151
+ const out = [];
152
+ let col = 0;
153
+ const continuation = () => {
154
+ const p = pending.get(col);
155
+ if (!p)
156
+ return;
157
+ out.push(`<w:tc>${tcPr(col, p.span, 'continue')}<w:p/></w:tc>`);
158
+ if (--p.rows <= 0)
159
+ pending.delete(col);
160
+ col += p.span;
161
+ };
162
+ for (const c of cells) {
163
+ while (pending.has(col))
164
+ continuation();
165
+ out.push(cellXml(c, col));
166
+ if (c.rowspan > 1)
167
+ pending.set(col, { rows: c.rowspan - 1, span: c.colspan });
168
+ col += c.colspan;
169
+ }
170
+ while (col < columns && pending.has(col))
171
+ continuation();
172
+ const trPr = r === 0 && t.headerRow ? '<w:trPr><w:tblHeader/></w:trPr>' : '';
173
+ return `<w:tr>${trPr}${out.join('')}</w:tr>`;
174
+ });
175
+ const tblPr = `<w:tblPr>${t.style ? `<w:tblStyle w:val="${attr(t.style)}"/>` : ''}<w:tblW w:w="0" w:type="auto"/></w:tblPr>`;
176
+ const grid = `<w:tblGrid>${widths.map((w) => `<w:gridCol w:w="${w}"/>`).join('')}</w:tblGrid>`;
177
+ return `<w:tbl>${tblPr}${grid}${rows.join('')}</w:tbl>`;
178
+ }
179
+ /** The blocks as a WordprocessingML fragment: `w:p` and `w:tbl` elements, the prefixes undeclared (`insertXml` declares them). */
180
+ export function blocksToXml(blocks, resolve = {}) {
181
+ return blocks.map((b) => (b.kind === 'table' ? tableXml(b, resolve) : paragraphXml(b, resolve))).join('');
182
+ }
@@ -0,0 +1,2 @@
1
+ export * from './convert.mjs';
2
+ export * from './elements.mjs';
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `@docx4j/core-ts/html`: HTML to an intermediate form, and that form to WordprocessingML (CR-005 section 8).
3
+ export * from './convert.mjs';
4
+ export * from './elements.mjs';
@@ -239,11 +239,18 @@ export class Body {
239
239
  * `values` fills it row by row. The columns are equal over the section's text width.
240
240
  */
241
241
  insertTable(rowCount, columnCount, location, values) {
242
+ var _a;
243
+ var _b;
242
244
  if (rowCount < 1 || columnCount < 1)
243
245
  throw new Docx4JException(`A table needs at least one row and one column; got ${rowCount} by ${columnCount}`);
244
246
  const rows = Array.from({ length: rowCount }, (_, r) => Array.from({ length: columnCount }, (_, c) => { var _a, _b; return (_b = (_a = values === null || values === void 0 ? void 0 : values[r]) === null || _a === void 0 ? void 0 : _a[c]) !== null && _b !== void 0 ? _b : ''; }));
245
247
  const width = writableWidthTwips(this.container);
246
248
  const element = tableOf(rows, width ? { width } : {});
249
+ // Word's own default (Insert Table, fixed column width Auto; the editor's check 42, Word 365): an
250
+ // automatic table width, no w:tblLayout, the cells in twips sharing the text width - the grid and
251
+ // cells the builder wrote, the table's width made automatic (CR-002 section 42.5 item 7).
252
+ const tblPr = ((_a = (_b = element.value).tblPr) !== null && _a !== void 0 ? _a : (_b.tblPr = {}));
253
+ tblPr.tblW = { w: 0, type: 'auto' };
247
254
  this.insertElement(element, location);
248
255
  return new Table(element, this.content, this);
249
256
  }
@@ -22,7 +22,9 @@ export interface ImageInfo {
22
22
  * The size and kind of an image, from its header: PNG (IHDR and pHYs), JPEG (SOFn and the JFIF
23
23
  * APP0 densities), GIF (the logical screen descriptor) and BMP (the DIB header, with its pixels
24
24
  * per metre). docx4j reads these through XML Graphics Commons' ImageInfo; this is the same
25
- * information, and the same 96 dpi default when a file declares none.
25
+ * information, and the same 96 dpi default when a file declares none. An SVG document is told by
26
+ * its root element and has no pixel size (both 0): it can be the part behind an existing picture
27
+ * (CR-005 section 9, a binding that keeps the drawing) but not be placed as a new one.
26
28
  */
27
29
  export declare function imageInfoOf(bytes: Uint8Array): ImageInfo;
28
30
  /** The natural size of an image in EMU: pixels over the resolution it declares. */
@@ -49,10 +51,23 @@ export interface NewPicture {
49
51
  imagePart: ImagePart;
50
52
  relId: string;
51
53
  }
54
+ /** A new image part, its relationship from the part it was added to, and what its header says. */
55
+ export interface NewImagePart {
56
+ imagePart: ImagePart;
57
+ relId: string;
58
+ info: ImageInfo;
59
+ }
52
60
  /**
53
61
  * Adds the image as a part of `source` (the main document part, or the header or footer the body
54
- * belongs to) with a relationship, and builds the run that shows it. docx4j:
55
- * `BinaryPartAbstractImage.createImagePart` then `createImageInline`.
62
+ * belongs to) with a relationship, and nothing else: docx4j `BinaryPartAbstractImage.createImagePart`
63
+ * (`BindingTraverserXSLT.createImagePartReturnRelId`, where a picture binding replaces an `r:embed`
64
+ * and keeps the drawing, CR-005 section 9). Throws, and adds nothing, when the bytes are not an
65
+ * image this package reads.
66
+ */
67
+ export declare function addImagePart(source: Part, base64: string): NewImagePart;
68
+ /**
69
+ * Adds the image as a part of `source` with a relationship (`addImagePart`), and builds the run
70
+ * that shows it. docx4j: `BinaryPartAbstractImage.createImagePart` then `createImageInline`.
56
71
  */
57
72
  export declare function addImage(source: Part, base64: string, scope: object, maxCx: number | undefined, options?: InlinePictureOptions): NewPicture;
58
73
  /** The writable width of a section in EMU (page width less the margins), for scaling. */
@@ -1,7 +1,7 @@
1
1
  import { inlinePicture } from '@docx4j/generated-objects-ts/builders/wml';
2
2
  import { Docx4JException } from '../../opc/exceptions.mjs';
3
3
  import { ContentTypes, IMAGE_CONTENT_TYPES_BY_EXTENSION } from '../../opc/ContentTypes.mjs';
4
- import { base64Decode, base64Encode } from '../../xml/dom.mjs';
4
+ import { base64Decode, base64Encode, decodeXmlText } from '../../xml/dom.mjs';
5
5
  import { ImagePart } from '../../parts/BinaryPart.mjs';
6
6
  import { PartName } from '../../opc/PartName.mjs';
7
7
  import { find, runOf } from './tree.mjs';
@@ -15,7 +15,9 @@ const EMU_PER_POINT = 12700;
15
15
  * The size and kind of an image, from its header: PNG (IHDR and pHYs), JPEG (SOFn and the JFIF
16
16
  * APP0 densities), GIF (the logical screen descriptor) and BMP (the DIB header, with its pixels
17
17
  * per metre). docx4j reads these through XML Graphics Commons' ImageInfo; this is the same
18
- * information, and the same 96 dpi default when a file declares none.
18
+ * information, and the same 96 dpi default when a file declares none. An SVG document is told by
19
+ * its root element and has no pixel size (both 0): it can be the part behind an existing picture
20
+ * (CR-005 section 9, a binding that keeps the drawing) but not be placed as a new one.
19
21
  */
20
22
  export function imageInfoOf(bytes) {
21
23
  const png = pngInfo(bytes);
@@ -30,8 +32,27 @@ export function imageInfoOf(bytes) {
30
32
  const bmp = bmpInfo(bytes);
31
33
  if (bmp)
32
34
  return bmp;
35
+ const svg = svgInfo(bytes);
36
+ if (svg)
37
+ return svg;
33
38
  const head = Array.from(bytes.subarray(0, 8)).map((b) => b.toString(16).padStart(2, '0')).join(' ');
34
- throw new Docx4JException(`Unsupported image format (first bytes ${head}); PNG, JPEG, GIF and BMP are read here`);
39
+ throw new Docx4JException(`Unsupported image format (first bytes ${head}); PNG, JPEG, GIF, BMP and SVG are read here`);
40
+ }
41
+ /** An SVG document: an XML declaration, comments and a doctype allowed before the `svg` root. */
42
+ function svgInfo(b) {
43
+ if (b.length < 5)
44
+ return undefined;
45
+ let head;
46
+ try {
47
+ head = decodeXmlText(b.subarray(0, Math.min(b.length, 4096)));
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ const body = head.replace(/^\uFEFF/, '').replace(/<\?xml[^>]*\?>/, '').replace(/<!--[\s\S]*?-->/g, '').replace(/<!DOCTYPE[^>]*>/i, '').trimStart();
53
+ if (!/^<(\w+:)?svg[\s>]/.test(body))
54
+ return undefined;
55
+ return { contentType: ContentTypes.IMAGE_SVG, extension: 'svg', widthPx: 0, heightPx: 0, dpiX: DEFAULT_DPI, dpiY: DEFAULT_DPI };
35
56
  }
36
57
  function be16(b, at) { return (b[at] << 8) | b[at + 1]; }
37
58
  function be32(b, at) { return ((b[at] << 24) >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3]; }
@@ -144,11 +165,12 @@ export function naturalSizeEmu(info) {
144
165
  }
145
166
  /**
146
167
  * Adds the image as a part of `source` (the main document part, or the header or footer the body
147
- * belongs to) with a relationship, and builds the run that shows it. docx4j:
148
- * `BinaryPartAbstractImage.createImagePart` then `createImageInline`.
168
+ * belongs to) with a relationship, and nothing else: docx4j `BinaryPartAbstractImage.createImagePart`
169
+ * (`BindingTraverserXSLT.createImagePartReturnRelId`, where a picture binding replaces an `r:embed`
170
+ * and keeps the drawing, CR-005 section 9). Throws, and adds nothing, when the bytes are not an
171
+ * image this package reads.
149
172
  */
150
- export function addImage(source, base64, scope, maxCx, options = {}) {
151
- var _a, _b;
173
+ export function addImagePart(source, base64) {
152
174
  const pkg = source.package;
153
175
  if (!pkg)
154
176
  throw new Docx4JException('This part is not in a package yet; add it before adding an image');
@@ -157,6 +179,21 @@ export function addImage(source, base64, scope, maxCx, options = {}) {
157
179
  const imagePart = new ImagePart(freeImageName(pkg, info.extension), info.contentType);
158
180
  imagePart.setBytes(bytes);
159
181
  const rel = source.addTargetPart(imagePart);
182
+ return { imagePart, relId: rel.id, info };
183
+ }
184
+ /**
185
+ * Adds the image as a part of `source` with a relationship (`addImagePart`), and builds the run
186
+ * that shows it. docx4j: `BinaryPartAbstractImage.createImagePart` then `createImageInline`.
187
+ */
188
+ export function addImage(source, base64, scope, maxCx, options = {}) {
189
+ var _a, _b;
190
+ const bytes = base64Decode(base64);
191
+ const info = imageInfoOf(bytes);
192
+ if (info.widthPx === 0 || info.heightPx === 0) {
193
+ throw new Docx4JException(`A ${info.extension.toUpperCase()} image has no pixel size to place it at; PNG, JPEG, GIF and BMP can be placed`);
194
+ }
195
+ const { imagePart, relId } = addImagePart(source, base64);
196
+ const rel = { id: relId };
160
197
  let { cx, cy } = naturalSizeEmu(info);
161
198
  if (options.width !== undefined) {
162
199
  const wanted = Math.round(options.width * EMU_PER_POINT);
@@ -4,6 +4,7 @@ import type { Body } from './Body.mjs';
4
4
  import type { Paragraph } from './Paragraph.mjs';
5
5
  import type { Range } from './Range.mjs';
6
6
  import { type ChangeTracker } from './tracking.mjs';
7
+ import { type TableLayoutMode } from './tableColumns.mjs';
7
8
  /**
8
9
  * A subset of Office JS `Word.Table` over a `w:tbl`. Rows are read through row-level content
9
10
  * controls (an OpenDoPE repeat wraps its `w:tr` in a `w:sdt`), so `rows` is what Word shows.
@@ -51,6 +52,30 @@ export declare class Table {
51
52
  * tracks changes the rows stay and take a `w:trPr/w:del` instead (CR-002 phase F).
52
53
  */
53
54
  deleteRows(rowIndex: number, rowCount?: number): void;
55
+ /**
56
+ * Adds columns at the start or the end (Office JS `Table.addColumns`), the new cells taking the
57
+ * properties and width of the column beside them - the one to the right of the boundary when
58
+ * there is one, else the left - in the table's layout mode as Word 365 writes it (CR-002 section
59
+ * 42, checks 39 and 40): fixed copies the width and grows the table past the margin, AutoFit to
60
+ * window rescales the grid and writes the cells' percentages again, AutoFit to contents stays
61
+ * automatic. `values` is by row, then by new column. Returns the new cells per row, an extension
62
+ * (Office JS returns nothing); a row whose skipped columns grew instead has none. Tracked, the
63
+ * table change is recorded as Office JS records one and the new cells' text is an insertion.
64
+ */
65
+ addColumns(location: 'Start' | 'End', columnCount: number, values?: string[][]): TableCell[][];
66
+ /**
67
+ * Removes `columnCount` grid columns from `columnIndex` (one by default; Office JS
68
+ * `Table.deleteColumns`): a cell covering only those columns goes, one spanning further shrinks.
69
+ * Tracked, the cells still go and the table change is recorded, as Office JS's deletion leaves
70
+ * it (section 42.3; check 40c; Word's own is not tracked at all, known issues entry 2).
71
+ */
72
+ deleteColumns(columnIndex: number, columnCount?: number): void;
73
+ /** Which of Word's three layout modes the table is in: fixed, AutoFit to window, or AutoFit to contents (extension). */
74
+ get layoutMode(): TableLayoutMode;
75
+ /** The section's text width in twips, for the window mode and a percentage width. */
76
+ textWidth(): number;
77
+ /** Views of new cells, row by row. */
78
+ cellViewsOf(cells: Element<wml.Tc>[][]): TableCell[][];
54
79
  /** Removes the table from its container; tracked, every row is marked deleted instead. */
55
80
  delete(): void;
56
81
  /** The package's change tracker while `changeTrackingMode` is on (CR-002 phase F). */
@@ -118,7 +143,20 @@ export declare class TableCell {
118
143
  insertText(text: string, location: 'Start' | 'End' | 'Replace'): Range;
119
144
  /** The cell width in points (w:tcW in twips), as Office JS reports it; 0 when it is not a fixed width. */
120
145
  get width(): number;
146
+ /**
147
+ * Written in the form the cell states, else its table, else twips (CR-002 section 42.2 item 4): a
148
+ * cell in percent takes the points as a share of the section's text width; `auto` becomes twips,
149
+ * a stated width being what was asked for.
150
+ */
121
151
  set width(points: number);
152
+ /**
153
+ * New columns before or after this cell (Office JS `TableCell.insertColumns`), the new cells
154
+ * taking this cell's properties and width, as `Table.addColumns` does. Returns the new cells per
155
+ * row (extension).
156
+ */
157
+ insertColumns(location: 'Before' | 'After', columnCount: number, values?: string[][]): TableCell[][];
158
+ /** Removes the grid columns this cell covers (Office JS `TableCell.deleteColumn`); tracked as `Table.deleteColumns` is. */
159
+ deleteColumn(): void;
122
160
  /** Office JS TableCell.columnWidth, the same value. */
123
161
  get columnWidth(): number;
124
162
  set columnWidth(points: number);
@@ -4,6 +4,7 @@ import { Docx4JException } from '../../opc/exceptions.mjs';
4
4
  import { typeNameOf, childrenOf, linkParents, textOf, rowsOf, cellsOf } from './tree.mjs';
5
5
  import { builtInOf, idOfBuiltIn, styleNameOf, styleIdOf } from './styles.mjs';
6
6
  import { trackInsertedBlocks } from './tracking.mjs';
7
+ import { insertGridColumns, deleteGridColumns, gridRangeOf, textWidthTwips, widthTypeOf, layoutModeOf } from './tableColumns.mjs';
7
8
  /** The default table width in twips (A4 with 2.54 cm margins), as the objects package's `tbl` builder uses. */
8
9
  const DEFAULT_WIDTH = 9026;
9
10
  const TWIPS_PER_POINT = 20;
@@ -128,6 +129,51 @@ export class Table {
128
129
  row.container.splice(i, 1);
129
130
  }
130
131
  }
132
+ /**
133
+ * Adds columns at the start or the end (Office JS `Table.addColumns`), the new cells taking the
134
+ * properties and width of the column beside them - the one to the right of the boundary when
135
+ * there is one, else the left - in the table's layout mode as Word 365 writes it (CR-002 section
136
+ * 42, checks 39 and 40): fixed copies the width and grows the table past the margin, AutoFit to
137
+ * window rescales the grid and writes the cells' percentages again, AutoFit to contents stays
138
+ * automatic. `values` is by row, then by new column. Returns the new cells per row, an extension
139
+ * (Office JS returns nothing); a row whose skipped columns grew instead has none. Tracked, the
140
+ * table change is recorded as Office JS records one and the new cells' text is an insertion.
141
+ */
142
+ addColumns(location, columnCount, values) {
143
+ const at = location === 'Start' ? 0 : this.columnWidths().length;
144
+ const inserted = insertGridColumns(this.tbl, this.textWidth(), at, columnCount, values, this.changeTracker);
145
+ return this.cellViewsOf(inserted.cells);
146
+ }
147
+ /**
148
+ * Removes `columnCount` grid columns from `columnIndex` (one by default; Office JS
149
+ * `Table.deleteColumns`): a cell covering only those columns goes, one spanning further shrinks.
150
+ * Tracked, the cells still go and the table change is recorded, as Office JS's deletion leaves
151
+ * it (section 42.3; check 40c; Word's own is not tracked at all, known issues entry 2).
152
+ */
153
+ deleteColumns(columnIndex, columnCount = 1) {
154
+ var _a, _b, _c;
155
+ deleteGridColumns(this.tbl, this.textWidth(), columnIndex, columnCount, this.changeTracker);
156
+ if (((_c = (_b = (_a = this.tbl.tblGrid) === null || _a === void 0 ? void 0 : _a.gridCol) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) === 0 || rowsOf(this.tbl).every((row) => cellsOf(row.element.value).length === 0))
157
+ this.delete();
158
+ }
159
+ /** Which of Word's three layout modes the table is in: fixed, AutoFit to window, or AutoFit to contents (extension). */
160
+ get layoutMode() {
161
+ return layoutModeOf(this.tbl);
162
+ }
163
+ /** The section's text width in twips, for the window mode and a percentage width. */
164
+ textWidth() {
165
+ return textWidthTwips(this.parentBody.container);
166
+ }
167
+ /** Views of new cells, row by row. */
168
+ cellViewsOf(cells) {
169
+ const rows = this.rows;
170
+ return cells.map((rowCells, i) => {
171
+ const row = rows[i];
172
+ if (!row || rowCells.length === 0)
173
+ return [];
174
+ return row.cells.filter((cell) => rowCells.includes(cell.element));
175
+ });
176
+ }
131
177
  /** Removes the table from its container; tracked, every row is marked deleted instead. */
132
178
  delete() {
133
179
  const tracker = this.changeTracker;
@@ -311,12 +357,44 @@ export class TableCell {
311
357
  const w = (_a = this.tc.tcPr) === null || _a === void 0 ? void 0 : _a.tcW;
312
358
  return (w === null || w === void 0 ? void 0 : w.type) === 'dxa' ? ((_b = w.w) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT : 0;
313
359
  }
360
+ /**
361
+ * Written in the form the cell states, else its table, else twips (CR-002 section 42.2 item 4): a
362
+ * cell in percent takes the points as a share of the section's text width; `auto` becomes twips,
363
+ * a stated width being what was asked for.
364
+ */
314
365
  set width(points) {
315
366
  var _a, _b;
316
367
  var _c, _d;
368
+ const twips = Math.round(points * TWIPS_PER_POINT);
317
369
  const tcW = ((_b = (_d = ((_a = (_c = this.tc).tcPr) !== null && _a !== void 0 ? _a : (_c.tcPr = {}))).tcW) !== null && _b !== void 0 ? _b : (_d.tcW = {}));
318
- tcW.w = Math.round(points * TWIPS_PER_POINT);
319
- tcW.type = 'dxa';
370
+ if (widthTypeOf(this.tc, this.parentTable.tbl) === 'pct') {
371
+ tcW.w = Math.round((twips * 5000) / this.parentTable.textWidth());
372
+ tcW.type = 'pct';
373
+ }
374
+ else {
375
+ tcW.w = twips;
376
+ tcW.type = 'dxa';
377
+ }
378
+ }
379
+ /**
380
+ * New columns before or after this cell (Office JS `TableCell.insertColumns`), the new cells
381
+ * taking this cell's properties and width, as `Table.addColumns` does. Returns the new cells per
382
+ * row (extension).
383
+ */
384
+ insertColumns(location, columnCount, values) {
385
+ const range = gridRangeOf(this.parentRow.tr, this.tc);
386
+ if (!range)
387
+ throw new Docx4JException('This cell is not in its row');
388
+ const table = this.parentTable;
389
+ const inserted = insertGridColumns(table.tbl, table.textWidth(), location === 'Before' ? range.start : range.end, columnCount, values, table.changeTracker);
390
+ return table.cellViewsOf(inserted.cells);
391
+ }
392
+ /** Removes the grid columns this cell covers (Office JS `TableCell.deleteColumn`); tracked as `Table.deleteColumns` is. */
393
+ deleteColumn() {
394
+ const range = gridRangeOf(this.parentRow.tr, this.tc);
395
+ if (!range)
396
+ throw new Docx4JException('This cell is not in its row');
397
+ this.parentTable.deleteColumns(range.start, range.end - range.start);
320
398
  }
321
399
  /** Office JS TableCell.columnWidth, the same value. */
322
400
  get columnWidth() {
@@ -7,7 +7,8 @@ export { List, ListItem, ListItemNotFoundError, NumberingFacade, listsOf, listLa
7
7
  export { type Author, type CommentContent, type CommentOptions, type CommentEntry, type CommentParts, type CommentPartsAccess, type CommentsExtensible, type CommentMarker, COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.mjs';
8
8
  export { Font, type UnderlineType, type RPrHolder, type FontTracking } from './Font.mjs';
9
9
  export { Table, TableRow, TableCell, cellOf } from './Table.mjs';
10
- export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, writableWidthEmu, type ImageFormat, type ImageInfo, type InlinePictureOptions, type NewPicture } from './InlinePicture.mjs';
10
+ export { layoutModeOf, textWidthTwips, gridWidthsOf, widthTypeOf, insertGridColumns, deleteGridColumns, gridRangeOf, type TableLayoutMode, type ColumnInsertion } from './tableColumns.mjs';
11
+ export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, addImagePart, writableWidthEmu, type ImageFormat, type ImageInfo, type InlinePictureOptions, type NewPicture, type NewImagePart } from './InlinePicture.mjs';
11
12
  export { ContentControl, collectControls, collectRunControls, W14_NS, W15_NS, type ContentControlType, type ContentControlForm, type ContentControlAppearance } from './ContentControl.mjs';
12
13
  export { contentOf, isFlatOpc, rewriteRelationshipIds, type OoxmlOptions } from './ooxml.mjs';
13
14
  export { type SearchOptions, searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
@@ -9,7 +9,8 @@ export { List, ListItem, ListItemNotFoundError, NumberingFacade, listsOf, listLa
9
9
  export { COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.mjs';
10
10
  export { Font } from './Font.mjs';
11
11
  export { Table, TableRow, TableCell, cellOf } from './Table.mjs';
12
- export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, writableWidthEmu } from './InlinePicture.mjs';
12
+ export { layoutModeOf, textWidthTwips, gridWidthsOf, widthTypeOf, insertGridColumns, deleteGridColumns, gridRangeOf } from './tableColumns.mjs';
13
+ export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, addImagePart, writableWidthEmu } from './InlinePicture.mjs';
13
14
  export { ContentControl, collectControls, collectRunControls, W14_NS, W15_NS } from './ContentControl.mjs';
14
15
  export { contentOf, isFlatOpc, rewriteRelationshipIds } from './ooxml.mjs';
15
16
  export { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
@@ -23,8 +23,10 @@ export declare class StylesFacade {
23
23
  *
24
24
  * A style already in the document is left exactly as it is - the document's own definition wins
25
25
  * over the default, always, since a consumer must not silently restyle a document it was asked to
26
- * add a footnote to. An id the defaults do not carry throws a `Docx4JException` naming it: the
27
- * caller asked for a definition nothing here has, and inventing one would be worse than saying so.
26
+ * add a footnote to. An id none of the sources carry - the defaults, the nine spliceable styles,
27
+ * the comment and placeholder styles, and Word's built-ins from docx4j's `KnownStyles.xml`
28
+ * (section 41) - throws a `Docx4JException` naming it: the caller asked for a definition nothing
29
+ * here has, and inventing one would be worse than saying so.
28
30
  * The whole closure is worked out before anything is written, so a call that throws has changed
29
31
  * nothing (CR-002 section 31).
30
32
  *
@@ -5,13 +5,16 @@ import { DEFAULT_STYLES_XML } from '../../parts/wml/defaultStyles.mjs';
5
5
  import { SPLICEABLE_STYLES_XML } from '../../parts/wml/spliceableStyles.mjs';
6
6
  import { commentStyles } from '../../parts/wml/commentStyles.mjs';
7
7
  import { placeholderStyle } from '../../parts/wml/placeholderStyle.mjs';
8
+ import { KNOWN_STYLES_XML } from '../../parts/wml/knownStyles.mjs';
8
9
  import { Docx4JException } from '../../opc/exceptions.mjs';
9
10
  /**
10
11
  * Where a definition is looked up, unmarshalled once and never handed out (a copy is spliced in):
11
12
  * the defaults `createPackage()` writes, and then the nine styles docx4j's own `styles.xml` carries
12
13
  * **commented out** - which is why a created document has no `FootnoteText` although the file
13
14
  * appears to contain one, and why this facade exists (CR-002 section 22.2) - with Word's two comment
14
- * styles and its `PlaceholderText` after them.
15
+ * styles and its `PlaceholderText` after them, and last Word's 164 built-ins as docx4j's
16
+ * `KnownStyles.xml` carries them (CR-002 section 41: `Heading5`, `Quote`, `HTMLCode`, ...), the
17
+ * list numbering some of them name dropped, since no numbering definition comes with them.
15
18
  */
16
19
  let sources;
17
20
  function styleSources() {
@@ -20,8 +23,17 @@ function styleSources() {
20
23
  const defaults = await read(DEFAULT_STYLES_XML);
21
24
  // and Word's two comment styles, which the comment parts add when they are made, for a caller
22
25
  // that makes the comment itself (CR-002 section 37), and Word's PlaceholderText, which a
23
- // placeholder run names (CR-002 section 40)
24
- return { defaults, all: [...defaults, ...(await read(SPLICEABLE_STYLES_XML)), ...commentStyles(), placeholderStyle()] };
26
+ // placeholder run names (CR-002 section 40); then the known styles (section 41), each without
27
+ // the w:numPr the file left on the headings and list styles: it names a w:numId of the document
28
+ // the file was taken from, which the document being spliced into need not have, and a heading
29
+ // spliced with it would join a list that is not there (the editor's copy drops it the same way)
30
+ const known = (await read(KNOWN_STYLES_XML)).map((style) => {
31
+ var _a;
32
+ if ((_a = style.pPr) === null || _a === void 0 ? void 0 : _a.numPr)
33
+ delete style.pPr.numPr;
34
+ return style;
35
+ });
36
+ return { defaults, all: [...defaults, ...(await read(SPLICEABLE_STYLES_XML)), ...commentStyles(), placeholderStyle(), ...known] };
25
37
  })()));
26
38
  }
27
39
  /**
@@ -42,8 +54,10 @@ export class StylesFacade {
42
54
  *
43
55
  * A style already in the document is left exactly as it is - the document's own definition wins
44
56
  * over the default, always, since a consumer must not silently restyle a document it was asked to
45
- * add a footnote to. An id the defaults do not carry throws a `Docx4JException` naming it: the
46
- * caller asked for a definition nothing here has, and inventing one would be worse than saying so.
57
+ * add a footnote to. An id none of the sources carry - the defaults, the nine spliceable styles,
58
+ * the comment and placeholder styles, and Word's built-ins from docx4j's `KnownStyles.xml`
59
+ * (section 41) - throws a `Docx4JException` naming it: the caller asked for a definition nothing
60
+ * here has, and inventing one would be worse than saying so.
47
61
  * The whole closure is worked out before anything is written, so a call that throws has changed
48
62
  * nothing (CR-002 section 31).
49
63
  *
@@ -105,7 +119,7 @@ function closureOf(wanted, present, source) {
105
119
  seen.add(id);
106
120
  const template = source.find((s) => s.styleId === id);
107
121
  if (template === undefined) {
108
- throw new Docx4JException(`No definition to splice for the style ${id}: neither docx4j's default styles, the nine it comments out, the two comment styles nor PlaceholderText carry one. Define it yourself on the styles part.`);
122
+ throw new Docx4JException(`No definition to splice for the style ${id}: neither docx4j's default styles, the nine it comments out, the two comment styles, PlaceholderText nor Word's built-ins in docx4j's KnownStyles.xml carry one. Define it yourself on the styles part.`);
109
123
  }
110
124
  // what it is based on and linked to must be there too, and before it reads better in the part
111
125
  for (const dependency of [(_a = template.basedOn) === null || _a === void 0 ? void 0 : _a.val, (_b = template.link) === null || _b === void 0 ? void 0 : _b.val]) {