@docx4j/core-ts 0.3.1 → 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 (57) 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 +9 -1
  9. package/dist/model/content/ContentControl.d.mts +5 -1
  10. package/dist/model/content/ContentControl.mjs +18 -5
  11. package/dist/model/content/InlinePicture.d.mts +18 -3
  12. package/dist/model/content/InlinePicture.mjs +44 -7
  13. package/dist/model/content/Paragraph.d.mts +11 -11
  14. package/dist/model/content/Paragraph.mjs +30 -43
  15. package/dist/model/content/Range.mjs +3 -2
  16. package/dist/model/content/Table.d.mts +38 -0
  17. package/dist/model/content/Table.mjs +80 -2
  18. package/dist/model/content/index.d.mts +2 -1
  19. package/dist/model/content/index.mjs +2 -1
  20. package/dist/model/content/stylesFacade.d.mts +4 -2
  21. package/dist/model/content/stylesFacade.mjs +23 -6
  22. package/dist/model/content/tableColumns.d.mts +48 -0
  23. package/dist/model/content/tableColumns.mjs +484 -0
  24. package/dist/model/content/tracking.d.mts +14 -0
  25. package/dist/model/content/tracking.mjs +19 -10
  26. package/dist/model/customxml/CustomXmlPartCollection.d.mts +6 -1
  27. package/dist/model/customxml/CustomXmlPartCollection.mjs +19 -3
  28. package/dist/model/customxml/XmlMapping.d.mts +20 -4
  29. package/dist/model/customxml/XmlMapping.mjs +44 -6
  30. package/dist/model/customxml/bindings.d.mts +13 -3
  31. package/dist/model/customxml/bindings.mjs +38 -7
  32. package/dist/model/customxml/index.d.mts +4 -1
  33. package/dist/model/customxml/index.mjs +4 -1
  34. package/dist/model/customxml/insert.d.mts +10 -0
  35. package/dist/model/customxml/insert.mjs +13 -0
  36. package/dist/model/customxml/opendope.d.mts +32 -0
  37. package/dist/model/customxml/opendope.mjs +56 -0
  38. package/dist/model/customxml/pictures.d.mts +54 -0
  39. package/dist/model/customxml/pictures.mjs +186 -0
  40. package/dist/model/customxml/xhtml.d.mts +42 -0
  41. package/dist/model/customxml/xhtml.mjs +162 -0
  42. package/dist/model/fonts/RunFontSelector.d.mts +8 -1
  43. package/dist/model/fonts/RunFontSelector.mjs +6 -5
  44. package/dist/model/properties/PropertyResolver.d.mts +110 -10
  45. package/dist/model/properties/PropertyResolver.mjs +300 -31
  46. package/dist/model/properties/index.d.mts +3 -0
  47. package/dist/model/properties/index.mjs +2 -0
  48. package/dist/model/properties/table.d.mts +155 -0
  49. package/dist/model/properties/table.mjs +315 -0
  50. package/dist/model/properties/tableStyleConditions.d.mts +101 -0
  51. package/dist/model/properties/tableStyleConditions.mjs +296 -0
  52. package/dist/packages/WordprocessingMLPackage.mjs +1 -0
  53. package/dist/parts/wml/knownStyles.d.mts +1 -0
  54. package/dist/parts/wml/knownStyles.mjs +2233 -0
  55. package/dist/parts/wml/placeholderStyle.d.mts +3 -0
  56. package/dist/parts/wml/placeholderStyle.mjs +13 -0
  57. package/package.json +7 -2
@@ -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';
@@ -6,7 +6,7 @@ import { Paragraph } from './Paragraph.mjs';
6
6
  import { Table } from './Table.mjs';
7
7
  import { ContentControl, collectControls } from './ContentControl.mjs';
8
8
  import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/wml';
9
- import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
9
+ import { controlIdScope, sdtKindFor, declareSdtExtensions } from '../customxml/insert.mjs';
10
10
  import { InlinePicture, addImage, writableWidthEmu } from './InlinePicture.mjs';
11
11
  import { contentOf } from './ooxml.mjs';
12
12
  import { expandReplacement } from './search.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
  }
@@ -748,6 +755,7 @@ export class Body {
748
755
  this.content.length = 0;
749
756
  this.content.push(sdt);
750
757
  linkParents(sdt, this.container);
758
+ declareSdtExtensions(this, sdt);
751
759
  // under tracking the control itself is the insertion, its content what it was (check 30)
752
760
  const tracker = this.changeTracker;
753
761
  if (tracker)
@@ -140,7 +140,11 @@ export declare class ContentControl {
140
140
  get repeatingSectionContentControl(): RepeatingSectionContentControl | undefined;
141
141
  /** The group view, for a `w:group` control (Office JS: an object with no members of its own). */
142
142
  get groupContentControl(): GroupContentControl | undefined;
143
- /** Adds or replaces a `w:sdtPr` child (extension). */
143
+ /**
144
+ * Adds or replaces a `w:sdtPr` child (extension). A `w15` element (`w15:appearance`, `w15:color`,
145
+ * `w15:dataBinding`, the repeating section) puts `w15` in the part's `mc:Ignorable`, as Word's own
146
+ * files have it: Word 2010 opens a part holding one only then (CR-002 section 40).
147
+ */
144
148
  putProperty(element: Element): void;
145
149
  /** Removes a `w:sdtPr` child (extension). */
146
150
  removeProperty(localPart: string, namespaceURI?: string): void;
@@ -6,7 +6,8 @@ import { marshalString, deepCopy } from '@docx4j/generated-objects-ts';
6
6
  // nothing from here at run time (types only), so this direction is the only one.
7
7
  import { XmlMapping } from '../customxml/XmlMapping.mjs';
8
8
  import { CheckboxContentControl, DatePickerContentControl, ListContentControl, PictureContentControl, RepeatingSectionContentControl, GroupContentControl, checkboxRun, CHECKBOX_FONT, } from '../customxml/kinds.mjs';
9
- import { runsForValue, updateFromControl, PLACEHOLDER_TEXT } from '../customxml/bindings.mjs';
9
+ import { runsForValue, updateFromControl, PLACEHOLDER_TEXT, PLACEHOLDER_STYLE } from '../customxml/bindings.mjs';
10
+ import { declareIgnorable } from './tracking.mjs';
10
11
  import { sdtProperty, sdtKindOf, t as textItem } from '@docx4j/generated-objects-ts/builders/wml';
11
12
  import { typeNameOf, childrenOf, textOf, runItemsOf, segmentsOf, linkParents, runOf, SDT_TYPES } from './tree.mjs';
12
13
  import { Range } from './Range.mjs';
@@ -429,7 +430,11 @@ export class ContentControl {
429
430
  get groupContentControl() {
430
431
  return sdtProperty(this.sdt.sdtPr, 'group') ? new GroupContentControl() : undefined;
431
432
  }
432
- /** Adds or replaces a `w:sdtPr` child (extension). */
433
+ /**
434
+ * Adds or replaces a `w:sdtPr` child (extension). A `w15` element (`w15:appearance`, `w15:color`,
435
+ * `w15:dataBinding`, the repeating section) puts `w15` in the part's `mc:Ignorable`, as Word's own
436
+ * files have it: Word 2010 opens a part holding one only then (CR-002 section 40).
437
+ */
433
438
  putProperty(element) {
434
439
  var _a, _b;
435
440
  var _c;
@@ -441,6 +446,8 @@ export class ContentControl {
441
446
  items[at] = element;
442
447
  else
443
448
  items.push(element);
449
+ if (element.name.namespaceURI === W15_NS)
450
+ declareIgnorable(this.parentBody.part, 'w15');
444
451
  }
445
452
  /** Removes a `w:sdtPr` child (extension). */
446
453
  removeProperty(localPart, namespaceURI) {
@@ -481,11 +488,11 @@ export class ContentControl {
481
488
  * the runs of a run-level one.
482
489
  */
483
490
  setBoundContent(runs, showingPlaceholder = false) {
484
- var _a, _b;
485
- var _c;
491
+ var _a, _b, _c, _d;
492
+ var _e;
486
493
  const sdt = this.sdt;
487
494
  (_a = sdt.sdtContent) !== null && _a !== void 0 ? _a : (sdt.sdtContent = {});
488
- const content = ((_b = (_c = sdt.sdtContent).content) !== null && _b !== void 0 ? _b : (_c.content = []));
495
+ const content = ((_b = (_e = sdt.sdtContent).content) !== null && _b !== void 0 ? _b : (_e.content = []));
489
496
  const first = content.find((e) => typeNameOf(e) !== undefined);
490
497
  if (this.form === 'Run' || (first && typeNameOf(first) === 'org_docx4j_wml.R')) {
491
498
  content.length = 0;
@@ -505,6 +512,12 @@ export class ContentControl {
505
512
  }
506
513
  }
507
514
  this.isShowingPlaceholder = showingPlaceholder;
515
+ // The placeholder run names the PlaceholderText style, which a document made here lacks: the
516
+ // package splices the definition in when it is saved, as it does a new hyperlink's style. Word
517
+ // 2010 drops a w:rStyle naming a style the part does not define (CR-002 section 40).
518
+ if (showingPlaceholder) {
519
+ (_d = (_c = this.parentBody.package_) === null || _c === void 0 ? void 0 : _c.requireStyles) === null || _d === void 0 ? void 0 : _d.call(_c, [PLACEHOLDER_STYLE]);
520
+ }
508
521
  }
509
522
  /** Shows a checkbox's glyph, as Word writes it: one run in the checkbox font (docx4j checkboxRun). */
510
523
  setCheckboxGlyph(symbol) {
@@ -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);
@@ -1,4 +1,5 @@
1
1
  import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
+ import type { CellContext } from '../properties/table.mjs';
2
3
  import { type Element, W_NS, type TextSegment, type TextViewOptions } from './tree.mjs';
3
4
  import { Font, type FontTracking } from './Font.mjs';
4
5
  import { Range } from './Range.mjs';
@@ -95,6 +96,16 @@ export declare class Paragraph {
95
96
  get effectivePPr(): wml.PPr;
96
97
  /** The paragraph mark's effective run properties (docx4j `getEffectiveParagraphMarkRPr`). */
97
98
  get effectiveParagraphMarkRPr(): wml.RPr;
99
+ /**
100
+ * Where this paragraph sits in a table, as far as its formatting goes (docx4j
101
+ * `PropertyResolver.cellContextOf`; CR-007): the table style its table resolves to and the
102
+ * conditional formats it is under. Undefined outside a table, and in a text box, note or comment
103
+ * reached from a cell. Every effective read of the paragraph and of its runs passes it, so a run
104
+ * in a header row the table style makes bold reads bold. Each read walks the table's rows; code
105
+ * resolving every paragraph of a large table should walk with a `TableContextTracker` instead.
106
+ * Needs the package's resolver, as the effective reads do.
107
+ */
108
+ get cellContext(): CellContext | undefined;
98
109
  /** The `w:pPr` a formatting read works from: the resolved one, or this paragraph's own. */
99
110
  private readPPr;
100
111
  /**
@@ -195,17 +206,6 @@ export declare class Paragraph {
195
206
  */
196
207
  delete(): void;
197
208
  private trackedDelete;
198
- /**
199
- * A paragraph mark deleted under tracking gives the next paragraph this paragraph's properties,
200
- * as a `w:pPrChange` on it recording its own: Word, at Delete at the end of a Heading 2 with a
201
- * Normal paragraph after it, wrote the next paragraph `w:pStyle Heading2` with
202
- * `<w:pPrChange><w:pPr/></w:pPrChange>`, and No Markup reads the joined line as a heading
203
- * throughout (the editor's `measure-word15.docx`, 2026-09-30; CR-002 section 38). The Delete
204
- * counterpart of check 29's Enter. Nothing where the two paragraphs' properties are the same
205
- * (check 22's deleted marks). Accepting the mark then joins into a paragraph with these
206
- * properties, and rejecting puts the next paragraph's back.
207
- */
208
- private giveMarkPropertiesToNext;
209
209
  /**
210
210
  * The paragraph's content and its mark marked deleted, the paragraph element itself kept:
211
211
  * what the cells of a deleted table row need (a `w:tc` must keep a block-level child, and Word
@@ -15,11 +15,11 @@ import { builtInOf, idOfBuiltIn, styleNameOf, styleIdOf } from './styles.mjs';
15
15
  import { cellOf } from './Table.mjs';
16
16
  import { ContentControl, collectRunControls } from './ContentControl.mjs';
17
17
  import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/wml';
18
- import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
18
+ import { controlIdScope, sdtKindFor, declareSdtExtensions } from '../customxml/insert.mjs';
19
19
  import { InlinePicture, addImage, writableWidthEmu } from './InlinePicture.mjs';
20
20
  import { contentOf } from './ooxml.mjs';
21
21
  import { commentApi } from './comments.mjs';
22
- import { ChangeTracker, copyRPr, markDeleted, toDeletedText, highestAnnotationId, canonical, restorePPr, wrapInsertedRuns, markHolderInserted } from './tracking.mjs';
22
+ import { ChangeTracker, copyRPr, markDeleted, toDeletedText, highestAnnotationId, canonical, restorePPr, wrapInsertedRuns, markHolderInserted, pruneParagraphProperties } from './tracking.mjs';
23
23
  import { trackedChangesOfParagraph } from './TrackedChange.mjs';
24
24
  import { isListItem, listOf, listOrNullObjectOf, listItemOf, listItemOrNullObjectOf, startNewList, attachToList, detachFromList, } from './List.mjs';
25
25
  const TWIPS_PER_POINT = 20;
@@ -125,11 +125,23 @@ export class Paragraph {
125
125
  * changing it. Needs the package's resolver; see `Body.propertyResolver`.
126
126
  */
127
127
  get effectivePPr() {
128
- return this.parentBody.propertyResolver.getEffectivePPr(this.p.pPr);
128
+ return this.parentBody.propertyResolver.getEffectivePPr(this.p.pPr, this.cellContext);
129
129
  }
130
130
  /** The paragraph mark's effective run properties (docx4j `getEffectiveParagraphMarkRPr`). */
131
131
  get effectiveParagraphMarkRPr() {
132
- return this.parentBody.propertyResolver.getEffectiveParagraphMarkRPr(this.p.pPr);
132
+ return this.parentBody.propertyResolver.getEffectiveParagraphMarkRPr(this.p.pPr, this.cellContext);
133
+ }
134
+ /**
135
+ * Where this paragraph sits in a table, as far as its formatting goes (docx4j
136
+ * `PropertyResolver.cellContextOf`; CR-007): the table style its table resolves to and the
137
+ * conditional formats it is under. Undefined outside a table, and in a text box, note or comment
138
+ * reached from a cell. Every effective read of the paragraph and of its runs passes it, so a run
139
+ * in a header row the table style makes bold reads bold. Each read walks the table's rows; code
140
+ * resolving every paragraph of a large table should walk with a `TableContextTracker` instead.
141
+ * Needs the package's resolver, as the effective reads do.
142
+ */
143
+ get cellContext() {
144
+ return this.parentBody.propertyResolver.cellContextOf(this.p);
133
145
  }
134
146
  /** The `w:pPr` a formatting read works from: the resolved one, or this paragraph's own. */
135
147
  readPPr(options) {
@@ -157,6 +169,7 @@ export class Paragraph {
157
169
  else
158
170
  pPr.jc = { val };
159
171
  this.settleFormatting();
172
+ pruneParagraphProperties(this.p); // no empty w:pPr left (the editor's finding on 0.3.1)
160
173
  }
161
174
  /**
162
175
  * The alignment the paragraph resolves to with no `w:jc` of its own: its style's, else the
@@ -172,7 +185,8 @@ export class Paragraph {
172
185
  if (!resolver)
173
186
  return undefined;
174
187
  const { jc: _jc, pPrChange: _change, PARENT: _parent, ...rest } = this.p.pPr;
175
- return alignmentOf(resolver.getEffectivePPr(rest), true);
188
+ // in a table cell what is inherited includes the table style's (CR-007)
189
+ return alignmentOf(resolver.getEffectivePPr(rest, resolver.cellContextOf(this.p)), true);
176
190
  }
177
191
  /** Indents and spacing in points, as Office JS; the effective values (CR-001 Phase B step 2). */
178
192
  get leftIndent() { var _a, _b; return ((_b = (_a = this.effectivePPr.ind) === null || _a === void 0 ? void 0 : _a.left) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT; }
@@ -222,7 +236,7 @@ export class Paragraph {
222
236
  const holders = () => runsOf(this.p).map((r) => r.value);
223
237
  if ((options === null || options === void 0 ? void 0 : options.direct) === true)
224
238
  return new Font(holders, () => this.fontTracking());
225
- return new Font(holders, () => this.fontTracking(), (rPr) => this.parentBody.propertyResolver.getEffectiveRPr(rPr, this.p.pPr), (rPr) => { var _a; return (_a = runFontSelectorOf(this.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); });
239
+ return new Font(holders, () => this.fontTracking(), (rPr) => this.parentBody.propertyResolver.getEffectiveRPr(rPr, this.p.pPr, this.cellContext), (rPr) => { var _a; return (_a = runFontSelectorOf(this.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); });
226
240
  }
227
241
  /** w14:paraId, the stable address Word gives paragraphs. */
228
242
  get paraId() {
@@ -426,42 +440,13 @@ export class Paragraph {
426
440
  return;
427
441
  }
428
442
  tracker.markParagraphDeleted(this.p);
429
- this.giveMarkPropertiesToNext(tracker);
430
- }
431
- /**
432
- * A paragraph mark deleted under tracking gives the next paragraph this paragraph's properties,
433
- * as a `w:pPrChange` on it recording its own: Word, at Delete at the end of a Heading 2 with a
434
- * Normal paragraph after it, wrote the next paragraph `w:pStyle Heading2` with
435
- * `<w:pPrChange><w:pPr/></w:pPrChange>`, and No Markup reads the joined line as a heading
436
- * throughout (the editor's `measure-word15.docx`, 2026-09-30; CR-002 section 38). The Delete
437
- * counterpart of check 29's Enter. Nothing where the two paragraphs' properties are the same
438
- * (check 22's deleted marks). Accepting the mark then joins into a paragraph with these
439
- * properties, and rejecting puts the next paragraph's back.
440
- */
441
- giveMarkPropertiesToNext(tracker) {
442
- const i = this.index;
443
- const next = i >= 0 ? this.container[i + 1] : undefined;
444
- if (!next || typeNameOf(next) !== 'org_docx4j_wml.P')
445
- return;
446
- const base = (pPr) => {
447
- const out = {};
448
- for (const [key, value] of Object.entries(pPr !== null && pPr !== void 0 ? pPr : {})) {
449
- if (key !== 'TYPE_NAME' && key !== 'PARENT' && key !== 'rPr' && key !== 'sectPr' && key !== 'pPrChange' && value !== undefined)
450
- out[key] = value;
451
- }
452
- return out;
453
- };
454
- const source = base(this.p.pPr);
455
- const nextP = next.value;
456
- if (canonical(source) === canonical(base(nextP.pPr)))
457
- return;
458
- const target = new Paragraph(next, this.container, this.parentBody).pPr();
459
- tracker.recordPPrChange(target);
460
- for (const key of Object.keys(base(target)))
461
- delete target[key];
462
- for (const [key, value] of Object.entries(source))
463
- target[key] = deepCopy(value);
464
- linkParents(target, nextP);
443
+ // Word, deleting a paragraph mark ALONE under tracking, gives the next paragraph this one's
444
+ // properties as a w:pPrChange (CR-002 section 38 item 2, the editor's measure-word15.docx), so
445
+ // that the joined line reads in them. This method deletes the whole paragraph, text and mark - the
446
+ // engine's only mark deletion, a Range being within one paragraph - and what Word writes for that
447
+ // is not measured (test/README.md check 33). 0.3.1 carried the properties here too, which turned
448
+ // an agent's new heading, put in after a paragraph it then deleted, into body text once accepted
449
+ // (the editor's finding on 0.3.1); until the check, nothing is carried.
465
450
  }
466
451
  /**
467
452
  * The paragraph's content and its mark marked deleted, the paragraph element itself kept:
@@ -533,8 +518,9 @@ export class Paragraph {
533
518
  if (!same) {
534
519
  const rest = (props) => canonical(Object.fromEntries(Object.entries(props).filter(([key]) => !FORMATTING_MEMBERS.has(key))));
535
520
  const resolver = (_b = this.parentBody.package_) === null || _b === void 0 ? void 0 : _b.propertyResolverOrUndefined;
521
+ const cell = resolver === null || resolver === void 0 ? void 0 : resolver.cellContextOf(this.p);
536
522
  same = resolver !== undefined && rest(current) === rest(before)
537
- && canonical(formattingOf(resolver.getEffectivePPr(current), true)) === canonical(formattingOf(resolver.getEffectivePPr(before), true));
523
+ && canonical(formattingOf(resolver.getEffectivePPr(current, cell), true)) === canonical(formattingOf(resolver.getEffectivePPr(before, cell), true));
538
524
  }
539
525
  if (same)
540
526
  restorePPr(this.p, recorded);
@@ -826,6 +812,7 @@ export class Paragraph {
826
812
  const sdt = sdtOf([this.element], { kind: sdtKindFor(kind), id: nextSdtId(controlIdScope(this.parentBody)), form: 'block' });
827
813
  this.container.splice(at, 1, sdt);
828
814
  linkParents(sdt, (_a = this.p.PARENT) !== null && _a !== void 0 ? _a : this.parentBody.container);
815
+ declareSdtExtensions(this.parentBody, sdt);
829
816
  // under tracking the control itself is the insertion, the paragraph what it was (check 30)
830
817
  const tracker = this.changeTracker;
831
818
  if (tracker)