@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
@@ -1,6 +1,7 @@
1
1
  import { deepCopy } from '@docx4j/generated-objects-ts';
2
2
  import * as el from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
3
3
  import { sdtProperty } from '@docx4j/generated-objects-ts/builders/wml';
4
+ import { bindPicture } from './pictures.mjs';
4
5
  /** The style Word gives placeholder text, and the text docx4j's placeholder.xml carries. */
5
6
  export const PLACEHOLDER_STYLE = 'PlaceholderText';
6
7
  export const PLACEHOLDER_TEXT = 'Click here to enter text.';
@@ -114,9 +115,10 @@ function dateOf(value) {
114
115
  /**
115
116
  * Pushes the custom XML value into one control (docx4j BindingHandler's per-sdt work). Returns
116
117
  * false when nothing was written: no binding, no part, an XPath that selects nothing, a kind
117
- * this phase does not handle (pictures, explicit rich text), or a node holding a Flat OPC package.
118
+ * this phase does not handle (explicit rich text), a node holding a Flat OPC package, or a
119
+ * picture control whose node holds no image (`notes` says which; CR-005 section 9).
118
120
  */
119
- export function applyBindingTo(control) {
121
+ export function applyBindingTo(control, notes) {
120
122
  var _a;
121
123
  const mapping = control.xmlMapping;
122
124
  if (!mapping.isMapped)
@@ -125,9 +127,14 @@ export function applyBindingTo(control) {
125
127
  if (!node)
126
128
  return false;
127
129
  const value = node.text.trim();
128
- // A picture binding carries base64 image data; docx4j replaces the a:blip embed. Deferred (section 12).
129
- if (control.type === 'Picture')
130
- return false;
130
+ // A picture control's node holds base64 image data: the picture's a:blip is pointed at a new
131
+ // image part and the drawing kept, as docx4j's bind.xslt mode picture3 does (CR-005 section 9).
132
+ if (control.type === 'Picture') {
133
+ const outcome = bindPicture(control, value, { kind: 'keep' }, control.title || control.tag || `picture control ${control.id}`);
134
+ if (outcome.note)
135
+ notes === null || notes === void 0 ? void 0 : notes.push(outcome.note);
136
+ return outcome.done;
137
+ }
131
138
  // Explicit rich text is bound from flat OPC or XHTML in docx4j; deferred, as it is there for this route.
132
139
  if (sdtProperty(control.sdt.sdtPr, 'richText') !== undefined)
133
140
  return false;
@@ -169,15 +176,18 @@ export function applyBindingTo(control) {
169
176
  /** `applyBindings` over a list of controls. */
170
177
  export function applyBindingsTo(controls) {
171
178
  const result = { bound: 0, updated: 0, skipped: 0 };
179
+ const notes = [];
172
180
  for (const control of controls) {
173
181
  if (!control.xmlMapping.isMapped)
174
182
  continue;
175
183
  result.bound++;
176
- if (applyBindingTo(control))
184
+ if (applyBindingTo(control, notes))
177
185
  result.updated++;
178
186
  else
179
187
  result.skipped++;
180
188
  }
189
+ if (notes.length > 0)
190
+ result.notes = notes;
181
191
  return result;
182
192
  }
183
193
  /**
@@ -194,6 +204,8 @@ export function updateFromControl(control) {
194
204
  const node = mapping.customXmlNode;
195
205
  if (!node)
196
206
  return false;
207
+ // A picture control is the asynchronous pass's (updateFromPictureControl, CR-005 section 9): its
208
+ // image's bytes go back to the node as base64, as Word 15 writes them (check 38).
197
209
  if (control.type === 'Picture')
198
210
  return false;
199
211
  if (sdtProperty(control.sdt.sdtPr, 'richText') !== undefined)
@@ -236,6 +248,9 @@ export function updateFromControls(controls) {
236
248
  for (const control of controls) {
237
249
  if (!control.xmlMapping.isMapped)
238
250
  continue;
251
+ // counted by the picture pass, which reads the image part (CR-005 section 9)
252
+ if (control.type === 'Picture')
253
+ continue;
239
254
  result.bound++;
240
255
  if (updateFromControl(control))
241
256
  result.updated++;
@@ -5,3 +5,6 @@ export { XmlMapping, type CustomXmlPartLookup } from './XmlMapping.mjs';
5
5
  export { CheckboxContentControl, DatePickerContentControl, ListContentControl, ContentControlListItem, PictureContentControl, RepeatingSectionContentControl, GroupContentControl, checkboxRun, CHECKED_SYMBOL, UNCHECKED_SYMBOL, CHECKBOX_FONT, } from './kinds.mjs';
6
6
  export { applyBindingsTo, applyBindingTo, updateFromControls, updateFromControl, runsForValue, formatDate, holdsFlatOpc, PLACEHOLDER_STYLE, PLACEHOLDER_TEXT, type BindingResult } from './bindings.mjs';
7
7
  export { controlIdScope, sdtKindFor, isRunLevel } from './insert.mjs';
8
+ export { tagParamsOf, xpathsEntriesOf, selectEntryNode, OPENDOPE_XPATHS_NS, XHTML_CONTENT_TYPE, PICTURE_HANDLER, type XPathsEntry } from './opendope.mjs';
9
+ export { applyXhtmlBindingTo, applyXhtmlBindingsTo, isXhtmlBound, type XhtmlHost, type XhtmlBindingOptions } from './xhtml.mjs';
10
+ export { applyPictureHandlerTo, applyPictureHandlersTo, isPictureHandlerBound, bindPicture, pictureWidthOf, updateFromPictureControl, updateFromPictureControls, type PictureWidth, type PictureOutcome } from './pictures.mjs';
@@ -7,3 +7,6 @@ export { XmlMapping } from './XmlMapping.mjs';
7
7
  export { CheckboxContentControl, DatePickerContentControl, ListContentControl, ContentControlListItem, PictureContentControl, RepeatingSectionContentControl, GroupContentControl, checkboxRun, CHECKED_SYMBOL, UNCHECKED_SYMBOL, CHECKBOX_FONT, } from './kinds.mjs';
8
8
  export { applyBindingsTo, applyBindingTo, updateFromControls, updateFromControl, runsForValue, formatDate, holdsFlatOpc, PLACEHOLDER_STYLE, PLACEHOLDER_TEXT } from './bindings.mjs';
9
9
  export { controlIdScope, sdtKindFor, isRunLevel } from './insert.mjs';
10
+ export { tagParamsOf, xpathsEntriesOf, selectEntryNode, OPENDOPE_XPATHS_NS, XHTML_CONTENT_TYPE, PICTURE_HANDLER } from './opendope.mjs';
11
+ export { applyXhtmlBindingTo, applyXhtmlBindingsTo, isXhtmlBound } from './xhtml.mjs';
12
+ export { applyPictureHandlerTo, applyPictureHandlersTo, isPictureHandlerBound, bindPicture, pictureWidthOf, updateFromPictureControl, updateFromPictureControls } from './pictures.mjs';
@@ -0,0 +1,32 @@
1
+ import type { CustomXmlPartLookup } from './XmlMapping.mjs';
2
+ import type { CustomXmlNode } from './CustomXmlPart.mjs';
3
+ export declare const OPENDOPE_XPATHS_NS = "http://opendope.org/xpaths";
4
+ export declare const XHTML_CONTENT_TYPE = "application/xhtml+xml";
5
+ /** The tag's `od:Handler` value for a picture (the specification's section 9.3; CR-005 section 9). */
6
+ export declare const PICTURE_HANDLER = "picture";
7
+ /** The tag's parameters, `key=value` pairs separated by `&`, in order; a value is as written (the content type keeps its `/` and `+`). */
8
+ export declare function tagParamsOf(tag: string): Map<string, string>;
9
+ /**
10
+ * An XPaths part entry: `<xpath id="x1"><dataBinding storeItemID xpath prefixMappings/></xpath>`.
11
+ * The attributes are unqualified (`xpaths.xsd`: `attributeFormDefault="unqualified"`; docx4j's
12
+ * `org.opendope.xpaths.Xpaths` and its invoice.docx write them so); a `w:`-qualified form, which
13
+ * this package's own check 37 and 38 files were made with, is read as well.
14
+ */
15
+ export interface XPathsEntry {
16
+ id: string;
17
+ storeItemID: string;
18
+ xpath: string;
19
+ prefixMappings: string;
20
+ }
21
+ /** The XPaths part's entries by id, from the first custom XML part in the OpenDoPE XPaths namespace; empty without one. */
22
+ export declare function xpathsEntriesOf(parts: CustomXmlPartLookup): Map<string, XPathsEntry>;
23
+ /**
24
+ * The node an `od:xpath` entry selects: the entry's XPath over the part its `storeItemID` names
25
+ * (the id compared without braces or case, as `getItem` does). What a tag-bound control's pass
26
+ * starts from; the note says why there is no node.
27
+ */
28
+ export declare function selectEntryNode(entryId: string, parts: CustomXmlPartLookup): {
29
+ node: CustomXmlNode;
30
+ } | {
31
+ note: string;
32
+ };
@@ -0,0 +1,56 @@
1
+ export const OPENDOPE_XPATHS_NS = 'http://opendope.org/xpaths';
2
+ export const XHTML_CONTENT_TYPE = 'application/xhtml+xml';
3
+ /** The tag's `od:Handler` value for a picture (the specification's section 9.3; CR-005 section 9). */
4
+ export const PICTURE_HANDLER = 'picture';
5
+ /** The tag's parameters, `key=value` pairs separated by `&`, in order; a value is as written (the content type keeps its `/` and `+`). */
6
+ export function tagParamsOf(tag) {
7
+ const out = new Map();
8
+ for (const pair of tag.split('&')) {
9
+ const eq = pair.indexOf('=');
10
+ if (eq <= 0)
11
+ continue;
12
+ const key = pair.slice(0, eq).trim();
13
+ if (!out.has(key))
14
+ out.set(key, pair.slice(eq + 1).trim());
15
+ }
16
+ return out;
17
+ }
18
+ /** The XPaths part's entries by id, from the first custom XML part in the OpenDoPE XPaths namespace; empty without one. */
19
+ export function xpathsEntriesOf(parts) {
20
+ const out = new Map();
21
+ const part = parts.items.find((p) => p.namespaceUri === OPENDOPE_XPATHS_NS);
22
+ if (!part)
23
+ return out;
24
+ const root = part.document.documentElement;
25
+ if (!root)
26
+ return out;
27
+ const entries = root.getElementsByTagNameNS(OPENDOPE_XPATHS_NS, 'xpath');
28
+ for (let i = 0; i < entries.length; i++) {
29
+ const entry = entries[i];
30
+ const id = entry.getAttribute('id');
31
+ const binding = entry.getElementsByTagNameNS(OPENDOPE_XPATHS_NS, 'dataBinding')[0];
32
+ if (!id || !binding)
33
+ continue;
34
+ const read = (name) => binding.getAttribute(name) || binding.getAttributeNS('http://schemas.openxmlformats.org/wordprocessingml/2006/main', name) || binding.getAttribute(`w:${name}`) || '';
35
+ out.set(id, { id, storeItemID: read('storeItemID'), xpath: read('xpath'), prefixMappings: read('prefixMappings') });
36
+ }
37
+ return out;
38
+ }
39
+ /**
40
+ * The node an `od:xpath` entry selects: the entry's XPath over the part its `storeItemID` names
41
+ * (the id compared without braces or case, as `getItem` does). What a tag-bound control's pass
42
+ * starts from; the note says why there is no node.
43
+ */
44
+ export function selectEntryNode(entryId, parts) {
45
+ var _a;
46
+ const entry = xpathsEntriesOf(parts).get(entryId);
47
+ if (!entry)
48
+ return { note: `od:xpath=${entryId}: no such entry in the XPaths part` };
49
+ const part = (_a = parts.getItem(entry.storeItemID)) !== null && _a !== void 0 ? _a : parts.items.find((p) => p.id.replace(/[{}]/g, '').toLowerCase() === entry.storeItemID.replace(/[{}]/g, '').toLowerCase());
50
+ if (!part)
51
+ return { note: `od:xpath=${entryId}: no custom XML part ${entry.storeItemID}` };
52
+ const node = part.selectSingleNode(entry.xpath, entry.prefixMappings);
53
+ if (!node)
54
+ return { note: `od:xpath=${entryId}: ${entry.xpath} selects nothing` };
55
+ return { node };
56
+ }
@@ -0,0 +1,54 @@
1
+ import type { ContentControl } from '../content/ContentControl.mjs';
2
+ import type { CustomXmlPartLookup } from './XmlMapping.mjs';
3
+ import type { BindingResult } from './bindings.mjs';
4
+ /**
5
+ * What the tag's `width` says: absent keeps the control's drawing and replaces its image; `auto`
6
+ * a new inline picture at its natural size, scaled down to the text width; `N` the same, scaled
7
+ * down to N twips when that is narrower than the text width (a value that is neither is `auto`,
8
+ * which is what docx4j does with one it cannot parse).
9
+ */
10
+ export type PictureWidth = {
11
+ kind: 'keep';
12
+ } | {
13
+ kind: 'auto';
14
+ } | {
15
+ kind: 'twips';
16
+ twips: number;
17
+ };
18
+ export interface PictureOutcome {
19
+ done: boolean;
20
+ /** Why the control was left as it was. */
21
+ note?: string;
22
+ }
23
+ /** Whether a control is bound to a picture through its tag (`od:Handler=picture` with an `od:xpath`). */
24
+ export declare function isPictureHandlerBound(control: ContentControl): boolean;
25
+ /** The tag's `width` parameter, read as `PictureWidth` says. */
26
+ export declare function pictureWidthOf(params: Map<string, string>): PictureWidth;
27
+ /**
28
+ * Binds one control to a value: base64 image data (a data URL accepted). With `keep`, the first
29
+ * `a:blip`'s `r:embed` is pointed at a new image part and the drawing is left as it is; a picture
30
+ * control holding no picture gets a new inline one at its natural size instead (docx4j's pre-v3
31
+ * fallback), a tag-bound control holding none is left and noted. Otherwise the control's content
32
+ * becomes one run holding a new inline picture, sized as `PictureWidth` says, in the first
33
+ * paragraph of a block-level control (its properties kept) or as a run-level control's content.
34
+ * An empty value, a value that is not an image this package reads (PNG, JPEG, GIF, BMP; SVG where
35
+ * the drawing is kept, since it has no pixel size to place a new picture at), and a body with no
36
+ * part each leave the control as it was, with a note. The image part the control showed before
37
+ * stays in the package, as docx4j leaves it.
38
+ */
39
+ export declare function bindPicture(control: ContentControl, value: string, width: PictureWidth, label: string): PictureOutcome;
40
+ /** Binds one tag-bound control (shape b): the entry's node, the tag's width. */
41
+ export declare function applyPictureHandlerTo(control: ContentControl, parts: CustomXmlPartLookup): PictureOutcome;
42
+ /** The picture pass over the tag-bound controls, after the text bindings: counted into the result, what was left said in its notes. */
43
+ export declare function applyPictureHandlersTo(controls: ContentControl[], parts: CustomXmlPartLookup, result: BindingResult): void;
44
+ /**
45
+ * The reverse direction for a mapped picture control (docx4j's `UpdateXmlFromDocumentSurface`
46
+ * skips pictures; Word 15 writes the picture back, check 38): the bytes of the image part the
47
+ * first `a:blip` embeds, as base64, into the node. Nothing is written when the node already holds
48
+ * those bytes (compared decoded, so Word's line-wrapped base64 counts as the same), when the
49
+ * control shows no picture, or when the blip's part is not an image part. Asynchronous, since a
50
+ * part's bytes may still be in the container.
51
+ */
52
+ export declare function updateFromPictureControl(control: ContentControl): Promise<boolean>;
53
+ /** `updateFromContentControls`' picture pass: the mapped picture controls, counted into the result. */
54
+ export declare function updateFromPictureControls(controls: ContentControl[], result: BindingResult): Promise<void>;
@@ -0,0 +1,186 @@
1
+ import { find } from '../content/tree.mjs';
2
+ import { addImage, addImagePart, writableWidthEmu } from '../content/InlinePicture.mjs';
3
+ import { ImagePart } from '../../parts/BinaryPart.mjs';
4
+ import { base64Decode, base64Encode } from '../../xml/dom.mjs';
5
+ import { enclosingCellOf } from '../properties/table.mjs';
6
+ import { tagParamsOf, selectEntryNode, PICTURE_HANDLER } from './opendope.mjs';
7
+ const EMU_PER_TWIP = 635;
8
+ const BLIP = 'org_docx4j_dml.CTBlip';
9
+ /** Whether a control is bound to a picture through its tag (`od:Handler=picture` with an `od:xpath`). */
10
+ export function isPictureHandlerBound(control) {
11
+ const params = tagParamsOf(control.tag);
12
+ return params.get('od:Handler') === PICTURE_HANDLER && params.has('od:xpath');
13
+ }
14
+ /** The tag's `width` parameter, read as `PictureWidth` says. */
15
+ export function pictureWidthOf(params) {
16
+ const width = params.get('width');
17
+ if (width === undefined)
18
+ return { kind: 'keep' };
19
+ const twips = /^\d+$/.test(width.trim()) ? Number(width) : 0;
20
+ return twips > 0 ? { kind: 'twips', twips } : { kind: 'auto' };
21
+ }
22
+ /** The node's value as base64: a data URL's prefix dropped, whitespace left to the lenient decoder. */
23
+ function base64Of(value) {
24
+ return value.replace(/^\s*data:[^,]*,/, '').trim();
25
+ }
26
+ function messageOf(e) {
27
+ return e instanceof Error ? e.message : String(e);
28
+ }
29
+ /** The first `a:blip` anywhere in the control's content, whose `r:embed` is the image. */
30
+ function firstBlipOf(control) {
31
+ var _a;
32
+ for (const blip of find((_a = control.sdt.sdtContent) !== null && _a !== void 0 ? _a : {}, BLIP))
33
+ return blip;
34
+ return undefined;
35
+ }
36
+ /** A width stated in twips (`dxa`, or no type), else undefined. */
37
+ function twipsOf(width) {
38
+ return width && typeof width.w === 'number' && width.w >= 0 && (width.type === undefined || width.type === 'dxa') ? width.w : undefined;
39
+ }
40
+ /**
41
+ * Word's default cell margins, 108 twips left and right (the `Normal Table` style's
42
+ * `w:tblCellMar`, which every table inherits), taken where neither the cell nor the table states
43
+ * one; a table style stating other margins is not read.
44
+ */
45
+ const DEFAULT_CELL_MARGIN_TWIPS = 108;
46
+ /** The cell's left and right margins together, in twips: `w:tcMar`, else the table's `w:tblCellMar`, else Word's defaults. */
47
+ function cellMarginsTwips(tc, tbl) {
48
+ var _a, _b, _c, _d, _e, _f, _g, _h;
49
+ const own = (_a = tc.tcPr) === null || _a === void 0 ? void 0 : _a.tcMar;
50
+ const table = (_b = tbl.tblPr) === null || _b === void 0 ? void 0 : _b.tblCellMar;
51
+ const left = (_e = (_c = twipsOf(own === null || own === void 0 ? void 0 : own.left)) !== null && _c !== void 0 ? _c : twipsOf((_d = table === null || table === void 0 ? void 0 : table.left) !== null && _d !== void 0 ? _d : table === null || table === void 0 ? void 0 : table.start)) !== null && _e !== void 0 ? _e : DEFAULT_CELL_MARGIN_TWIPS;
52
+ const right = (_h = (_f = twipsOf(own === null || own === void 0 ? void 0 : own.right)) !== null && _f !== void 0 ? _f : twipsOf((_g = table === null || table === void 0 ? void 0 : table.right) !== null && _g !== void 0 ? _g : table === null || table === void 0 ? void 0 : table.end)) !== null && _h !== void 0 ? _h : DEFAULT_CELL_MARGIN_TWIPS;
53
+ return left + right;
54
+ }
55
+ /**
56
+ * The width a new picture must fit, in EMU: the cell's `w:tcW` less its margins when the control
57
+ * is in a table cell and the width is stated in twips (a percentage or `auto` falls to the page),
58
+ * else the page's text width; narrowed to `width=N`. Undefined where nothing states a width (a
59
+ * header's body has no `w:sectPr`): the natural size then.
60
+ */
61
+ function maxWidthEmu(control, width) {
62
+ var _a;
63
+ const cell = enclosingCellOf(control.element.value);
64
+ const tcW = cell ? twipsOf((_a = cell.tc.tcPr) === null || _a === void 0 ? void 0 : _a.tcW) : undefined;
65
+ const inner = cell && tcW !== undefined ? tcW - cellMarginsTwips(cell.tc, cell.tbl) : undefined;
66
+ const cellEmu = inner !== undefined && inner > 0 ? inner * EMU_PER_TWIP : undefined;
67
+ const text = cellEmu !== null && cellEmu !== void 0 ? cellEmu : writableWidthEmu(control.parentBody.container);
68
+ if (width.kind === 'twips') {
69
+ const wanted = width.twips * EMU_PER_TWIP;
70
+ return text === undefined ? wanted : Math.min(wanted, text);
71
+ }
72
+ return text;
73
+ }
74
+ /**
75
+ * Binds one control to a value: base64 image data (a data URL accepted). With `keep`, the first
76
+ * `a:blip`'s `r:embed` is pointed at a new image part and the drawing is left as it is; a picture
77
+ * control holding no picture gets a new inline one at its natural size instead (docx4j's pre-v3
78
+ * fallback), a tag-bound control holding none is left and noted. Otherwise the control's content
79
+ * becomes one run holding a new inline picture, sized as `PictureWidth` says, in the first
80
+ * paragraph of a block-level control (its properties kept) or as a run-level control's content.
81
+ * An empty value, a value that is not an image this package reads (PNG, JPEG, GIF, BMP; SVG where
82
+ * the drawing is kept, since it has no pixel size to place a new picture at), and a body with no
83
+ * part each leave the control as it was, with a note. The image part the control showed before
84
+ * stays in the package, as docx4j leaves it.
85
+ */
86
+ export function bindPicture(control, value, width, label) {
87
+ const base64 = base64Of(value);
88
+ if (base64 === '')
89
+ return { done: false, note: `${label}: the node is empty; the control is left as it is` };
90
+ const source = control.parentBody.part;
91
+ if (!source)
92
+ return { done: false, note: `${label}: the control's body has no part to hold an image` };
93
+ if (width.kind === 'keep') {
94
+ const blip = firstBlipOf(control);
95
+ if (blip) {
96
+ try {
97
+ blip.embed = addImagePart(source, base64).relId;
98
+ }
99
+ catch (e) {
100
+ return { done: false, note: `${label}: not an image this package reads (${messageOf(e)})` };
101
+ }
102
+ control.isShowingPlaceholder = false;
103
+ return { done: true };
104
+ }
105
+ if (control.type !== 'Picture')
106
+ return { done: false, note: `${label}: the control holds no a:blip to point at the image (od:Handler=picture without width keeps the drawing)` };
107
+ width = { kind: 'auto' };
108
+ }
109
+ let run;
110
+ try {
111
+ run = addImage(source, base64, control.parentBody.container, maxWidthEmu(control, width)).run;
112
+ }
113
+ catch (e) {
114
+ return { done: false, note: `${label}: not an image this package reads (${messageOf(e)})` };
115
+ }
116
+ control.setBoundContent([run]);
117
+ return { done: true };
118
+ }
119
+ /** Binds one tag-bound control (shape b): the entry's node, the tag's width. */
120
+ export function applyPictureHandlerTo(control, parts) {
121
+ var _a;
122
+ const params = tagParamsOf(control.tag);
123
+ const entryId = (_a = params.get('od:xpath')) !== null && _a !== void 0 ? _a : '';
124
+ const selected = selectEntryNode(entryId, parts);
125
+ if ('note' in selected)
126
+ return { done: false, note: selected.note };
127
+ return bindPicture(control, selected.node.text, pictureWidthOf(params), `od:xpath=${entryId}`);
128
+ }
129
+ /** The picture pass over the tag-bound controls, after the text bindings: counted into the result, what was left said in its notes. */
130
+ export function applyPictureHandlersTo(controls, parts, result) {
131
+ var _a;
132
+ for (const control of controls) {
133
+ if (!isPictureHandlerBound(control))
134
+ continue;
135
+ result.bound++;
136
+ const outcome = applyPictureHandlerTo(control, parts);
137
+ if (outcome.done)
138
+ result.updated++;
139
+ else
140
+ result.skipped++;
141
+ if (outcome.note)
142
+ ((_a = result.notes) !== null && _a !== void 0 ? _a : (result.notes = [])).push(outcome.note);
143
+ }
144
+ }
145
+ /**
146
+ * The reverse direction for a mapped picture control (docx4j's `UpdateXmlFromDocumentSurface`
147
+ * skips pictures; Word 15 writes the picture back, check 38): the bytes of the image part the
148
+ * first `a:blip` embeds, as base64, into the node. Nothing is written when the node already holds
149
+ * those bytes (compared decoded, so Word's line-wrapped base64 counts as the same), when the
150
+ * control shows no picture, or when the blip's part is not an image part. Asynchronous, since a
151
+ * part's bytes may still be in the container.
152
+ */
153
+ export async function updateFromPictureControl(control) {
154
+ var _a;
155
+ const mapping = control.xmlMapping;
156
+ if (!mapping.isMapped || control.type !== 'Picture')
157
+ return false;
158
+ const node = mapping.customXmlNode;
159
+ if (!node)
160
+ return false;
161
+ const blip = firstBlipOf(control);
162
+ const source = control.parentBody.part;
163
+ if (!(blip === null || blip === void 0 ? void 0 : blip.embed) || !source)
164
+ return false;
165
+ const part = (_a = source.relationshipsPart) === null || _a === void 0 ? void 0 : _a.getPart(blip.embed);
166
+ if (!(part instanceof ImagePart))
167
+ return false;
168
+ const bytes = await part.getBytes();
169
+ const held = base64Decode(node.text);
170
+ if (held.length === bytes.length && held.every((b, i) => b === bytes[i]))
171
+ return false;
172
+ node.text = base64Encode(bytes);
173
+ return true;
174
+ }
175
+ /** `updateFromContentControls`' picture pass: the mapped picture controls, counted into the result. */
176
+ export async function updateFromPictureControls(controls, result) {
177
+ for (const control of controls) {
178
+ if (control.type !== 'Picture' || !control.xmlMapping.isMapped)
179
+ continue;
180
+ result.bound++;
181
+ if (await updateFromPictureControl(control))
182
+ result.updated++;
183
+ else
184
+ result.skipped++;
185
+ }
186
+ }
@@ -0,0 +1,42 @@
1
+ import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
+ import type { PropertyResolver } from '../properties/index.mjs';
3
+ import type { ContentControl } from '../content/ContentControl.mjs';
4
+ import type { CustomXmlPartLookup } from './XmlMapping.mjs';
5
+ import type { BindingResult } from './bindings.mjs';
6
+ /** What the XHTML pass needs of the package. */
7
+ export interface XhtmlHost {
8
+ numbering: {
9
+ newList(options?: {
10
+ bullet?: boolean;
11
+ }): Promise<string>;
12
+ restart(numId: string): string;
13
+ };
14
+ styles: {
15
+ ensure(ids: readonly string[]): Promise<string[]>;
16
+ };
17
+ requireStyles(ids: Iterable<string>): void;
18
+ getPropertyResolver(): Promise<PropertyResolver>;
19
+ getMainDocumentPart(): {
20
+ styleDefinitionsPart?: {
21
+ readContents(): Promise<wml.Styles>;
22
+ } | undefined;
23
+ };
24
+ }
25
+ export interface XhtmlBindingOptions {
26
+ /** The HTML parser: a browser's `DOMParser` by default; jsdom's or linkedom's in Node. */
27
+ parser?: (html: string) => Document;
28
+ }
29
+ /** Whether a control is bound to XHTML through its tag. */
30
+ export declare function isXhtmlBound(control: ContentControl): boolean;
31
+ /**
32
+ * Binds one XHTML control. Returns what was done, or a note saying why not: no entry, no part, no
33
+ * node, no parser in this runtime. The content the markup gave that the control cannot hold is in
34
+ * `dropped` (a run-level control's blocks after the first, and what the converter dropped).
35
+ */
36
+ export declare function applyXhtmlBindingTo(control: ContentControl, parts: CustomXmlPartLookup, host: XhtmlHost, options?: XhtmlBindingOptions): Promise<{
37
+ done: boolean;
38
+ note?: string;
39
+ dropped?: Record<string, number>;
40
+ }>;
41
+ /** The XHTML pass over the controls, after the text bindings: counted into the result, what was left said in its notes. */
42
+ export declare function applyXhtmlBindingsTo(controls: ContentControl[], parts: CustomXmlPartLookup, host: XhtmlHost, result: BindingResult, options?: XhtmlBindingOptions): Promise<void>;
@@ -0,0 +1,162 @@
1
+ import { deepCopy } from '@docx4j/generated-objects-ts';
2
+ import { Namespaces } from '../../parts/Namespaces.mjs';
3
+ import { typeNameOf, linkParents, childrenOf } from '../content/tree.mjs';
4
+ import { contentOf } from '../content/ooxml.mjs';
5
+ import { convertHtml, styleLookupOf } from '../../html/convert.mjs';
6
+ import { blocksToXml, needsOf, HYPERLINK_STYLE } from '../../html/elements.mjs';
7
+ import { tagParamsOf, xpathsEntriesOf, XHTML_CONTENT_TYPE } from './opendope.mjs';
8
+ /** Whether a control is bound to XHTML through its tag. */
9
+ export function isXhtmlBound(control) {
10
+ const params = tagParamsOf(control.tag);
11
+ return params.get('od:ContentType') === XHTML_CONTENT_TYPE && params.has('od:xpath');
12
+ }
13
+ async function lookupFor(host) {
14
+ var _a, _b, _c;
15
+ const entries = [];
16
+ const part = host.getMainDocumentPart().styleDefinitionsPart;
17
+ if (part) {
18
+ const styles = await part.readContents();
19
+ for (const s of (_a = styles.style) !== null && _a !== void 0 ? _a : []) {
20
+ if (!s.styleId)
21
+ continue;
22
+ if (s.type !== undefined && s.type !== 'paragraph' && s.type !== 'character')
23
+ continue;
24
+ if (s.hidden !== undefined && s.hidden.val !== false)
25
+ continue;
26
+ entries.push({ id: s.styleId, name: (_c = (_b = s.name) === null || _b === void 0 ? void 0 : _b.val) !== null && _c !== void 0 ? _c : s.styleId });
27
+ }
28
+ }
29
+ const resolver = await host.getPropertyResolver();
30
+ return styleLookupOf(entries, resolver.getStyle('TableGrid') ? ['TableGrid'] : []);
31
+ }
32
+ /** The object behind an element: a parsed fragment's elements are wrapped (`{ name, value }`), the tree's own may not be. */
33
+ function innerOf(element) {
34
+ const e = element;
35
+ return e.name !== undefined && e.value !== undefined && typeof e.value === 'object' ? e.value : element;
36
+ }
37
+ /** The control's run properties onto every run the markup left without any (a hyperlink's runs included). */
38
+ function applyRunProperties(elements, rPr) {
39
+ if (!rPr)
40
+ return;
41
+ const visit = (items) => {
42
+ if (!items)
43
+ return;
44
+ for (const item of items) {
45
+ const name = typeNameOf(item);
46
+ const inner = innerOf(item);
47
+ if (name === 'org_docx4j_wml.R') {
48
+ const run = inner;
49
+ if (!run.rPr)
50
+ run.rPr = deepCopy(rPr);
51
+ }
52
+ else if (name !== undefined) {
53
+ visit(childrenOf(inner));
54
+ }
55
+ }
56
+ };
57
+ visit(elements);
58
+ }
59
+ function inlineContentOf(paragraph) {
60
+ var _a;
61
+ const content = (_a = childrenOf(innerOf(paragraph))) !== null && _a !== void 0 ? _a : [];
62
+ return content.filter((item) => { const n = typeNameOf(item); return n === 'org_docx4j_wml.R' || n === 'org_docx4j_wml.P.Hyperlink'; });
63
+ }
64
+ /**
65
+ * Binds one XHTML control. Returns what was done, or a note saying why not: no entry, no part, no
66
+ * node, no parser in this runtime. The content the markup gave that the control cannot hold is in
67
+ * `dropped` (a run-level control's blocks after the first, and what the converter dropped).
68
+ */
69
+ export async function applyXhtmlBindingTo(control, parts, host, options = {}) {
70
+ var _a, _b, _c, _d, _e, _f, _g, _h;
71
+ var _j;
72
+ const params = tagParamsOf(control.tag);
73
+ const entryId = params.get('od:xpath');
74
+ const entry = xpathsEntriesOf(parts).get(entryId);
75
+ if (!entry)
76
+ return { done: false, note: `od:xpath=${entryId}: no such entry in the XPaths part` };
77
+ const part = (_a = parts.getItem(entry.storeItemID)) !== null && _a !== void 0 ? _a : parts.items.find((p) => p.id.replace(/[{}]/g, '').toLowerCase() === entry.storeItemID.replace(/[{}]/g, '').toLowerCase());
78
+ if (!part)
79
+ return { done: false, note: `od:xpath=${entryId}: no custom XML part ${entry.storeItemID}` };
80
+ const node = part.selectSingleNode(entry.xpath, entry.prefixMappings);
81
+ if (!node)
82
+ return { done: false, note: `od:xpath=${entryId}: ${entry.xpath} selects nothing` };
83
+ const parser = (_b = options.parser) !== null && _b !== void 0 ? _b : (typeof DOMParser === 'function' ? (h) => new DOMParser().parseFromString(h, 'text/html') : undefined);
84
+ if (!parser)
85
+ return { done: false, note: `od:xpath=${entryId}: XHTML needs an HTML parser (applyBindings({ html: { parser } }))` };
86
+ const converted = convertHtml(node.text, await lookupFor(host), { parser });
87
+ const dropped = { ...converted.report.dropped };
88
+ const { blocks } = converted;
89
+ // What the document must give first: the styles the markup names, a definition per list, a relationship per link.
90
+ if (converted.report.stylesWanted.length > 0) {
91
+ try {
92
+ await host.styles.ensure(converted.report.stylesWanted);
93
+ }
94
+ catch {
95
+ dropped['styles the defaults lack'] = ((_c = dropped['styles the defaults lack']) !== null && _c !== void 0 ? _c : 0) + converted.report.stylesWanted.length;
96
+ }
97
+ }
98
+ const needs = needsOf(blocks);
99
+ const lists = new Map();
100
+ let firstNumbered;
101
+ for (const { key, bullet } of needs.lists) {
102
+ if (bullet)
103
+ lists.set(key, await host.numbering.newList({ bullet: true }));
104
+ else if (firstNumbered === undefined) {
105
+ firstNumbered = await host.numbering.newList({ bullet: false });
106
+ lists.set(key, firstNumbered);
107
+ }
108
+ else
109
+ lists.set(key, host.numbering.restart(firstNumbered));
110
+ }
111
+ const links = new Map();
112
+ const rels = needs.links.length > 0 ? (_d = control.parentBody.part) === null || _d === void 0 ? void 0 : _d.getRelationshipsPart(true) : undefined;
113
+ if (rels) {
114
+ for (const address of needs.links)
115
+ links.set(address, rels.addExternalRelationship(Namespaces.HYPERLINK, address).id);
116
+ host.requireStyles([HYPERLINK_STYLE]);
117
+ }
118
+ else if (needs.links.length > 0)
119
+ dropped['links'] = ((_e = dropped['links']) !== null && _e !== void 0 ? _e : 0) + needs.links.length;
120
+ const elements = await contentOf(blocksToXml(blocks, { numId: (key) => lists.get(key), relId: (address) => links.get(address) }));
121
+ applyRunProperties(elements, control.runProperties);
122
+ const sdt = control.sdt;
123
+ (_f = sdt.sdtContent) !== null && _f !== void 0 ? _f : (sdt.sdtContent = {});
124
+ const content = ((_g = (_j = sdt.sdtContent).content) !== null && _g !== void 0 ? _g : (_j.content = []));
125
+ if (control.form === 'Run') {
126
+ // A run-level control holds inline content: the first paragraph's, the rest reported.
127
+ const first = elements.find((e) => typeNameOf(e) === 'org_docx4j_wml.P');
128
+ const inlines = first ? inlineContentOf(first) : [];
129
+ const rest = elements.length - (first ? 1 : 0);
130
+ if (rest > 0)
131
+ dropped['blocks after the first, in a run-level control'] = ((_h = dropped['blocks after the first, in a run-level control']) !== null && _h !== void 0 ? _h : 0) + rest;
132
+ content.length = 0;
133
+ content.push(...inlines);
134
+ linkParents(content, sdt.sdtContent);
135
+ }
136
+ else {
137
+ content.length = 0;
138
+ content.push(...elements);
139
+ linkParents(content, sdt.sdtContent);
140
+ }
141
+ control.isShowingPlaceholder = false;
142
+ return Object.keys(dropped).length > 0 ? { done: true, dropped } : { done: true };
143
+ }
144
+ /** The XHTML pass over the controls, after the text bindings: counted into the result, what was left said in its notes. */
145
+ export async function applyXhtmlBindingsTo(controls, parts, host, result, options = {}) {
146
+ var _a, _b;
147
+ for (const control of controls) {
148
+ if (!isXhtmlBound(control))
149
+ continue;
150
+ result.bound++;
151
+ const outcome = await applyXhtmlBindingTo(control, parts, host, options);
152
+ if (outcome.done)
153
+ result.updated++;
154
+ else
155
+ result.skipped++;
156
+ if (outcome.note)
157
+ ((_a = result.notes) !== null && _a !== void 0 ? _a : (result.notes = [])).push(outcome.note);
158
+ if (outcome.dropped)
159
+ for (const [kind, n] of Object.entries(outcome.dropped))
160
+ ((_b = result.notes) !== null && _b !== void 0 ? _b : (result.notes = [])).push(`${control.tag}: dropped ${kind} (${n})`);
161
+ }
162
+ }
@@ -621,6 +621,7 @@ export class WordprocessingMLPackage extends OpcPackage {
621
621
  return bodies;
622
622
  },
623
623
  customXmlRelationshipSource: () => this.getMainDocumentPart(),
624
+ xhtmlHost: this,
624
625
  })));
625
626
  }
626
627
  get progId() {