@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.
- package/README.md +3 -1
- package/dist/html/convert.d.mts +116 -0
- package/dist/html/convert.mjs +541 -0
- package/dist/html/elements.d.mts +19 -0
- package/dist/html/elements.mjs +182 -0
- package/dist/html/index.d.mts +2 -0
- package/dist/html/index.mjs +4 -0
- package/dist/model/content/Body.mjs +7 -0
- package/dist/model/content/InlinePicture.d.mts +18 -3
- package/dist/model/content/InlinePicture.mjs +44 -7
- package/dist/model/content/Table.d.mts +38 -0
- package/dist/model/content/Table.mjs +80 -2
- package/dist/model/content/index.d.mts +2 -1
- package/dist/model/content/index.mjs +2 -1
- package/dist/model/content/stylesFacade.d.mts +4 -2
- package/dist/model/content/stylesFacade.mjs +20 -6
- package/dist/model/content/tableColumns.d.mts +48 -0
- package/dist/model/content/tableColumns.mjs +484 -0
- package/dist/model/customxml/CustomXmlPartCollection.d.mts +6 -1
- package/dist/model/customxml/CustomXmlPartCollection.mjs +19 -3
- package/dist/model/customxml/bindings.d.mts +5 -2
- package/dist/model/customxml/bindings.mjs +21 -6
- package/dist/model/customxml/index.d.mts +3 -0
- package/dist/model/customxml/index.mjs +3 -0
- package/dist/model/customxml/opendope.d.mts +32 -0
- package/dist/model/customxml/opendope.mjs +56 -0
- package/dist/model/customxml/pictures.d.mts +54 -0
- package/dist/model/customxml/pictures.mjs +186 -0
- package/dist/model/customxml/xhtml.d.mts +42 -0
- package/dist/model/customxml/xhtml.mjs +162 -0
- package/dist/packages/WordprocessingMLPackage.mjs +1 -0
- package/dist/parts/wml/knownStyles.d.mts +1 -0
- package/dist/parts/wml/knownStyles.mjs +2233 -0
- 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 (
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|