@arbium/enterprise-agent-connect 0.1.2-dev.d2aa5ca → 0.2.1-dev.7b4e802
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 +19 -7
- package/dist/connectors/adestra/connector.json +336 -0
- package/dist/connectors/adobe-admin-console/connector.json +212 -0
- package/dist/connectors/argo-cd/connector.json +210 -0
- package/dist/connectors/automox/connector.json +376 -0
- package/dist/connectors/churnzero/connector.json +238 -0
- package/dist/connectors/confluence/connector.json +168 -0
- package/dist/connectors/cvent/connector.json +287 -0
- package/dist/connectors/datadog/connector.json +188 -0
- package/dist/connectors/drata/connector.json +447 -0
- package/dist/connectors/druva/connector.json +258 -0
- package/dist/connectors/dynamics-365/connector.json +16 -1
- package/dist/connectors/dynamics-365/index.d.ts +1 -4
- package/dist/connectors/dynamics-365/index.js.map +1 -1
- package/dist/connectors/entra/index.d.ts +1 -1
- package/dist/connectors/forsta-decipher/connector.json +164 -0
- package/dist/connectors/gitlab/connector.json +263 -0
- package/dist/connectors/gong/vendor.d.ts +1 -1
- package/dist/connectors/gong/vendor.js +24 -32
- package/dist/connectors/gong/vendor.js.map +1 -1
- package/dist/connectors/grafana/connector.json +162 -0
- package/dist/connectors/http/index.d.ts +7 -7
- package/dist/connectors/http/index.js +66 -9
- package/dist/connectors/http/index.js.map +1 -1
- package/dist/connectors/http/openapi.d.ts +62 -0
- package/dist/connectors/http/openapi.js +342 -0
- package/dist/connectors/http/openapi.js.map +1 -0
- package/dist/connectors/intercom/connector.json +249 -0
- package/dist/connectors/intune/index.d.ts +1 -1
- package/dist/connectors/jamf/vendor.js +5 -1
- package/dist/connectors/jamf/vendor.js.map +1 -1
- package/dist/connectors/jira/connector.json +3 -3
- package/dist/connectors/jira/vendor.d.ts +1 -1
- package/dist/connectors/jira/vendor.js +8 -2
- package/dist/connectors/jira/vendor.js.map +1 -1
- package/dist/connectors/lusha/connector.json +59 -0
- package/dist/connectors/lusha/index.d.ts +8 -0
- package/dist/connectors/lusha/index.js +7 -0
- package/dist/connectors/lusha/index.js.map +1 -0
- package/dist/connectors/lusha/tools.d.ts +2 -0
- package/dist/connectors/lusha/tools.js +113 -0
- package/dist/connectors/lusha/tools.js.map +1 -0
- package/dist/connectors/lusha/vendor.d.ts +4 -0
- package/dist/connectors/lusha/vendor.js +72 -0
- package/dist/connectors/lusha/vendor.js.map +1 -0
- package/dist/connectors/microsoft/client.d.ts +11 -3
- package/dist/connectors/microsoft/client.js +62 -8
- package/dist/connectors/microsoft/client.js.map +1 -1
- package/dist/connectors/netsuite/connector.json +4 -2
- package/dist/connectors/netsuite/vendor.d.ts +1 -1
- package/dist/connectors/netsuite/vendor.js +5 -6
- package/dist/connectors/netsuite/vendor.js.map +1 -1
- package/dist/connectors/odata/index.d.ts +1 -4
- package/dist/connectors/odata/index.js +12 -5
- package/dist/connectors/odata/index.js.map +1 -1
- package/dist/connectors/okta/connector.json +3 -2
- package/dist/connectors/okta/vendor.d.ts +1 -1
- package/dist/connectors/okta/vendor.js +7 -6
- package/dist/connectors/okta/vendor.js.map +1 -1
- package/dist/connectors/outlook-calendar/index.d.ts +1 -1
- package/dist/connectors/outlook-mail/index.d.ts +1 -1
- package/dist/connectors/pagerduty/connector.json +247 -0
- package/dist/connectors/qualys/connector.json +271 -0
- package/dist/connectors/safebase/connector.json +221 -0
- package/dist/connectors/salesforce/vendor.d.ts +1 -1
- package/dist/connectors/salesforce/vendor.js +9 -3
- package/dist/connectors/salesforce/vendor.js.map +1 -1
- package/dist/connectors/sap-s4hana/connector.json +1 -0
- package/dist/connectors/servicenow/vendor.d.ts +0 -1
- package/dist/connectors/servicenow/vendor.js +7 -10
- package/dist/connectors/servicenow/vendor.js.map +1 -1
- package/dist/connectors/sharepoint-files/connector.json +34 -3
- package/dist/connectors/sharepoint-files/documents.d.ts +7 -0
- package/dist/connectors/sharepoint-files/documents.js +212 -0
- package/dist/connectors/sharepoint-files/documents.js.map +1 -0
- package/dist/connectors/sharepoint-files/docx-worker.d.ts +7 -0
- package/dist/connectors/sharepoint-files/docx-worker.js +41 -0
- package/dist/connectors/sharepoint-files/docx-worker.js.map +1 -0
- package/dist/connectors/sharepoint-files/docx.d.ts +96 -0
- package/dist/connectors/sharepoint-files/docx.js +823 -0
- package/dist/connectors/sharepoint-files/docx.js.map +1 -0
- package/dist/connectors/sharepoint-files/index.d.ts +1 -4
- package/dist/connectors/sharepoint-files/index.js +2 -1
- package/dist/connectors/sharepoint-files/index.js.map +1 -1
- package/dist/connectors/sharepoint-files/ooxml.d.ts +19 -0
- package/dist/connectors/sharepoint-files/ooxml.js +179 -0
- package/dist/connectors/sharepoint-files/ooxml.js.map +1 -0
- package/dist/connectors/sharepoint-files/paging.d.ts +15 -0
- package/dist/connectors/sharepoint-files/paging.js +87 -0
- package/dist/connectors/sharepoint-files/paging.js.map +1 -0
- package/dist/connectors/sharepoint-files/parse-host.mjs +78 -0
- package/dist/connectors/sharepoint-files/pdf-worker.mjs +108 -0
- package/dist/connectors/sharepoint-files/pdf.d.ts +16 -0
- package/dist/connectors/sharepoint-files/pdf.js +19 -0
- package/dist/connectors/sharepoint-files/pdf.js.map +1 -0
- package/dist/connectors/sharepoint-files/sandbox.d.ts +19 -0
- package/dist/connectors/sharepoint-files/sandbox.js +176 -0
- package/dist/connectors/sharepoint-files/sandbox.js.map +1 -0
- package/dist/connectors/sharepoint-files/tools.d.ts +6 -0
- package/dist/connectors/sharepoint-files/tools.js +79 -4
- package/dist/connectors/sharepoint-files/tools.js.map +1 -1
- package/dist/connectors/slack/vendor.d.ts +1 -1
- package/dist/connectors/slack/vendor.js +32 -38
- package/dist/connectors/slack/vendor.js.map +1 -1
- package/dist/connectors/soap/index.d.ts +1 -4
- package/dist/connectors/soap/index.js +4 -3
- package/dist/connectors/soap/index.js.map +1 -1
- package/dist/connectors/sonarqube/connector.json +165 -0
- package/dist/connectors/sprout-social/connector.json +158 -0
- package/dist/connectors/sql/index.d.ts +1 -4
- package/dist/connectors/sql/index.js +2 -0
- package/dist/connectors/sql/index.js.map +1 -1
- package/dist/connectors/synthesia/connector.json +113 -0
- package/dist/connectors/teams/index.d.ts +1 -1
- package/dist/connectors/teams/tools.d.ts +6 -1
- package/dist/connectors/teams/tools.js +3 -2
- package/dist/connectors/teams/tools.js.map +1 -1
- package/dist/connectors/wordpress/connector.json +203 -0
- package/dist/connectors/workday/connector.json +6 -4
- package/dist/connectors/workday/vendor.d.ts +1 -1
- package/dist/connectors/workday/vendor.js +8 -4
- package/dist/connectors/workday/vendor.js.map +1 -1
- package/dist/connectors/zephyr/connector.json +192 -0
- package/dist/packages/connector-kit/src/common.d.ts +4 -5
- package/dist/packages/connector-kit/src/common.js +4 -1
- package/dist/packages/connector-kit/src/common.js.map +1 -1
- package/dist/packages/connector-kit/src/connector.d.ts +82 -34
- package/dist/packages/connector-kit/src/connector.js +163 -21
- package/dist/packages/connector-kit/src/connector.js.map +1 -1
- package/dist/packages/connector-kit/src/fetch.d.ts +6 -0
- package/dist/packages/connector-kit/src/fetch.js +44 -0
- package/dist/packages/connector-kit/src/fetch.js.map +1 -0
- package/dist/packages/connector-kit/src/index.d.ts +3 -3
- package/dist/packages/connector-kit/src/index.js +1 -0
- package/dist/packages/connector-kit/src/index.js.map +1 -1
- package/dist/packages/connector-kit/src/oauth.d.ts +3 -1
- package/dist/packages/connector-kit/src/oauth.js +16 -6
- package/dist/packages/connector-kit/src/oauth.js.map +1 -1
- package/dist/packages/connector-kit/src/types.d.ts +4 -1
- package/dist/packages/connector-kit/src/types.js.map +1 -1
- package/dist/packages/server/migrations/0008_internal_apps.sql +16 -0
- package/dist/packages/server/migrations/rollback/to-legacy.sql +2 -0
- package/dist/packages/server/src/api.d.ts +2486 -257
- package/dist/packages/server/src/api.js +84 -1
- package/dist/packages/server/src/api.js.map +1 -1
- package/dist/packages/server/src/app.js +26 -1
- package/dist/packages/server/src/app.js.map +1 -1
- package/dist/packages/server/src/builtin.d.ts +7 -3
- package/dist/packages/server/src/builtin.js +32 -0
- package/dist/packages/server/src/builtin.js.map +1 -1
- package/dist/packages/server/src/events/runtime.js +17 -10
- package/dist/packages/server/src/events/runtime.js.map +1 -1
- package/dist/packages/server/src/events/store.d.ts +2 -2
- package/dist/packages/server/src/index.d.ts +3 -3
- package/dist/packages/server/src/index.js +32 -22
- package/dist/packages/server/src/index.js.map +1 -1
- package/dist/packages/server/src/service.d.ts +1134 -14
- package/dist/packages/server/src/service.js +266 -21
- package/dist/packages/server/src/service.js.map +1 -1
- package/dist/packages/server/src/store.d.ts +43 -4
- package/dist/packages/server/src/store.js +79 -0
- package/dist/packages/server/src/store.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import { Denied } from "../../packages/connector-kit/src/common.js";
|
|
2
|
+
import { child, children, isW, makeZip, on, openZip, parseXml, serializeXml, val, W, } from "./ooxml.js";
|
|
3
|
+
import { clip } from "./sandbox.js";
|
|
4
|
+
// Word files (.docx, and .docm read as data: macros are never run, only XML is read) as
|
|
5
|
+
// structured text, and new Word files made from a template by filling its named fields
|
|
6
|
+
// (content controls, w:sdt, with a w:tag). Hidden text is reported apart from the text, since it
|
|
7
|
+
// is a way to plant instructions for an agent; finding it is best effort. Called inside the
|
|
8
|
+
// docx worker (docx-worker.ts), never on the server's own thread.
|
|
9
|
+
export const MAX_TEXT = 20_000;
|
|
10
|
+
export const MAX_ROWS = 200;
|
|
11
|
+
export const MAX_CELLS = 12;
|
|
12
|
+
export const MAX_TAGS = 200;
|
|
13
|
+
// Word itself allows 63 columns.
|
|
14
|
+
export const MAX_ROW_CELLS = 64;
|
|
15
|
+
export const MAX_TABLE_ROWS = 10_000;
|
|
16
|
+
// Tags written into [[field:<tag>]] markers.
|
|
17
|
+
export const PLAIN_TAG = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
18
|
+
const DOCUMENT = "word/document.xml";
|
|
19
|
+
const STYLES = "word/styles.xml";
|
|
20
|
+
const HEADER_FOOTER = /^word\/(header|footer)(\d*)\.xml$/;
|
|
21
|
+
const READ_PARTS = (name) => name === DOCUMENT ||
|
|
22
|
+
name === STYLES ||
|
|
23
|
+
name === "word/comments.xml" ||
|
|
24
|
+
HEADER_FOOTER.test(name);
|
|
25
|
+
const MC = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
26
|
+
const MARK_OPEN = (tag) => `[[field:${tag}]]`;
|
|
27
|
+
const MARK_CLOSE = "[[/field]]";
|
|
28
|
+
// Each "[" followed by another, so "[[[" cannot leave a "[[" behind.
|
|
29
|
+
const MARK_LIKE = /\[(?=\[)/g;
|
|
30
|
+
// The properties that can hide a run, from one rPr. Both branches of markup compatibility
|
|
31
|
+
// (mc:Choice, mc:Fallback) count: a run hidden in either is reported.
|
|
32
|
+
function collect(rPr, into, branch = false) {
|
|
33
|
+
if (!rPr)
|
|
34
|
+
return;
|
|
35
|
+
for (let c = rPr.firstChild; c; c = c.nextSibling) {
|
|
36
|
+
if (c.nodeType !== 1)
|
|
37
|
+
continue;
|
|
38
|
+
const e = c;
|
|
39
|
+
if (e.namespaceURI === MC && e.localName === "AlternateContent") {
|
|
40
|
+
for (let b = e.firstChild; b; b = b.nextSibling)
|
|
41
|
+
if (b.nodeType === 1 && b.namespaceURI === MC)
|
|
42
|
+
collect(b, into, true);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!isW(e))
|
|
46
|
+
continue;
|
|
47
|
+
switch (e.localName) {
|
|
48
|
+
case "vanish":
|
|
49
|
+
if (on(e))
|
|
50
|
+
into.vanish = true;
|
|
51
|
+
else if (!branch)
|
|
52
|
+
into.vanish = false;
|
|
53
|
+
break;
|
|
54
|
+
case "color":
|
|
55
|
+
into.color = (val(e) ?? "").toUpperCase();
|
|
56
|
+
into.theme = (e.getAttributeNS(W, "themeColor") ?? "").toLowerCase() || undefined;
|
|
57
|
+
break;
|
|
58
|
+
case "sz": {
|
|
59
|
+
const v = val(e);
|
|
60
|
+
if (v && /^\d+$/.test(v))
|
|
61
|
+
into.sz = Number(v);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "w": {
|
|
65
|
+
const v = val(e);
|
|
66
|
+
if (v && /^\d+%?$/.test(v))
|
|
67
|
+
into.scale = Number(v.replace("%", ""));
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "shd":
|
|
71
|
+
into.fill = (e.getAttributeNS(W, "fill") ?? "").toUpperCase() || undefined;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** word/styles.xml: document defaults, styles by id (with basedOn), the default paragraph style. */
|
|
77
|
+
export class Styles {
|
|
78
|
+
styles = new Map();
|
|
79
|
+
cache = new Map();
|
|
80
|
+
defaults = {};
|
|
81
|
+
defaultParagraph = null;
|
|
82
|
+
constructor(doc) {
|
|
83
|
+
const root = doc?.documentElement;
|
|
84
|
+
if (!root || !isW(root, "styles"))
|
|
85
|
+
return;
|
|
86
|
+
const defaults = child(root, "docDefaults");
|
|
87
|
+
const rPrDefault = defaults ? child(defaults, "rPrDefault") : null;
|
|
88
|
+
collect(rPrDefault ? child(rPrDefault, "rPr") : null, this.defaults);
|
|
89
|
+
for (const s of children(root, "style")) {
|
|
90
|
+
const id = s.getAttributeNS(W, "styleId");
|
|
91
|
+
if (!id)
|
|
92
|
+
continue;
|
|
93
|
+
const type = s.getAttributeNS(W, "type") ?? "paragraph";
|
|
94
|
+
this.styles.set(id, { type, basedOn: val(child(s, "basedOn")), rPr: child(s, "rPr") });
|
|
95
|
+
const isDefault = (s.getAttributeNS(W, "default") ?? "").toLowerCase();
|
|
96
|
+
if (type === "paragraph" && ["1", "true", "on"].includes(isDefault))
|
|
97
|
+
this.defaultParagraph = id;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// A style's properties with what it is based on underneath, at most 20 levels.
|
|
101
|
+
chain(id) {
|
|
102
|
+
if (!id)
|
|
103
|
+
return {};
|
|
104
|
+
const hit = this.cache.get(id);
|
|
105
|
+
if (hit)
|
|
106
|
+
return hit;
|
|
107
|
+
const ids = [];
|
|
108
|
+
for (let at = id; at && ids.length < 20 && !ids.includes(at);) {
|
|
109
|
+
ids.push(at);
|
|
110
|
+
at = this.styles.get(at)?.basedOn ?? null;
|
|
111
|
+
}
|
|
112
|
+
const props = {};
|
|
113
|
+
for (const x of ids.reverse())
|
|
114
|
+
collect(this.styles.get(x)?.rPr ?? null, props);
|
|
115
|
+
this.cache.set(id, props);
|
|
116
|
+
return props;
|
|
117
|
+
}
|
|
118
|
+
/** A run's effective properties: defaults, paragraph style, character style, its own. */
|
|
119
|
+
run(rPr, paragraphStyle) {
|
|
120
|
+
const props = { ...this.defaults };
|
|
121
|
+
Object.assign(props, this.chain(paragraphStyle ?? this.defaultParagraph));
|
|
122
|
+
const rStyle = rPr ? val(child(rPr, "rStyle")) : null;
|
|
123
|
+
Object.assign(props, this.chain(rStyle));
|
|
124
|
+
collect(rPr, props);
|
|
125
|
+
return props;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const NO_STYLES = new Styles(null);
|
|
129
|
+
const nearWhite = (hex) => {
|
|
130
|
+
if (!hex || !/^[0-9A-F]{6}$/.test(hex))
|
|
131
|
+
return false;
|
|
132
|
+
return [0, 2, 4].every((i) => parseInt(hex.slice(i, i + 2), 16) >= 0xf0);
|
|
133
|
+
};
|
|
134
|
+
const WHITE_THEMES = new Set(["background1", "light1"]);
|
|
135
|
+
/** Why a run's text is hidden (best effort): hidden, white or near-white, or tiny. */
|
|
136
|
+
export function hiddenReasons(props) {
|
|
137
|
+
const reasons = [];
|
|
138
|
+
if (props.vanish)
|
|
139
|
+
reasons.push("hidden text");
|
|
140
|
+
if ((props.theme && WHITE_THEMES.has(props.theme)) || (!props.theme && nearWhite(props.color)))
|
|
141
|
+
reasons.push("white text");
|
|
142
|
+
else if (props.fill && props.color && props.color !== "AUTO" && props.fill === props.color)
|
|
143
|
+
reasons.push("text the colour of its shading");
|
|
144
|
+
if ((props.sz !== undefined && props.sz <= 5) || (props.scale !== undefined && props.scale <= 10))
|
|
145
|
+
reasons.push("tiny text");
|
|
146
|
+
return reasons;
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------------------------
|
|
149
|
+
// Reading
|
|
150
|
+
const tagOf = (sdt) => {
|
|
151
|
+
const pr = child(sdt, "sdtPr");
|
|
152
|
+
const tag = pr ? val(child(pr, "tag")) : null;
|
|
153
|
+
return tag ? tag : null;
|
|
154
|
+
};
|
|
155
|
+
/** Where a field sits: inside a paragraph, around paragraphs or tables, rows, or cells. */
|
|
156
|
+
export function fieldPlace(sdt) {
|
|
157
|
+
const parent = sdt.parentNode;
|
|
158
|
+
if (isW(parent, "tbl"))
|
|
159
|
+
return "row";
|
|
160
|
+
if (isW(parent, "tr"))
|
|
161
|
+
return "cell";
|
|
162
|
+
for (let n = parent; n; n = n.parentNode) {
|
|
163
|
+
if (isW(n, "p"))
|
|
164
|
+
return "inline";
|
|
165
|
+
if (isW(n, "body") || isW(n, "tc") || isW(n, "hdr") || isW(n, "ftr"))
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
return "block";
|
|
169
|
+
}
|
|
170
|
+
const contentOf = (sdt) => child(sdt, "sdtContent");
|
|
171
|
+
const fieldTable = (sdt) => {
|
|
172
|
+
const content = contentOf(sdt);
|
|
173
|
+
return content && fieldPlace(sdt) === "block" ? child(content, "tbl") : null;
|
|
174
|
+
};
|
|
175
|
+
// What one reading records. A reader without one only produces text (a field's value, a
|
|
176
|
+
// comment's text): nothing is counted twice.
|
|
177
|
+
class Recorder {
|
|
178
|
+
hidden = [];
|
|
179
|
+
fields = new Map();
|
|
180
|
+
anchors = new Map();
|
|
181
|
+
open = new Set();
|
|
182
|
+
insertions = 0;
|
|
183
|
+
deletions = 0;
|
|
184
|
+
textBoxes = false;
|
|
185
|
+
neutralized = false;
|
|
186
|
+
}
|
|
187
|
+
class Reader {
|
|
188
|
+
marks;
|
|
189
|
+
rec;
|
|
190
|
+
styles;
|
|
191
|
+
// The block fields being read, outermost first: their markers wrap each paragraph.
|
|
192
|
+
blockTags = [];
|
|
193
|
+
hiddenHere = null;
|
|
194
|
+
paragraphStyle = null;
|
|
195
|
+
constructor(marks, rec, styles) {
|
|
196
|
+
this.marks = marks;
|
|
197
|
+
this.rec = rec;
|
|
198
|
+
this.styles = styles;
|
|
199
|
+
}
|
|
200
|
+
render(parts) {
|
|
201
|
+
let out = "";
|
|
202
|
+
let text = "";
|
|
203
|
+
const flush = () => {
|
|
204
|
+
if (MARK_LIKE.test(text)) {
|
|
205
|
+
MARK_LIKE.lastIndex = 0;
|
|
206
|
+
if (this.rec)
|
|
207
|
+
this.rec.neutralized = true;
|
|
208
|
+
text = text.replace(MARK_LIKE, "[ ");
|
|
209
|
+
}
|
|
210
|
+
out += text;
|
|
211
|
+
text = "";
|
|
212
|
+
};
|
|
213
|
+
for (const p of parts)
|
|
214
|
+
if (typeof p === "string")
|
|
215
|
+
text += p;
|
|
216
|
+
else {
|
|
217
|
+
flush();
|
|
218
|
+
out += p.mark;
|
|
219
|
+
}
|
|
220
|
+
flush();
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
checkTag(tag) {
|
|
224
|
+
if (this.marks && !PLAIN_TAG.test(tag))
|
|
225
|
+
throw new Denied("A field has a tag that is not a plain name (letters, digits, _ . - up to 64), so fields cannot be marked: read it without mark_fields");
|
|
226
|
+
}
|
|
227
|
+
visible(acc, s) {
|
|
228
|
+
acc.plain += s;
|
|
229
|
+
const last = acc.parts.length - 1;
|
|
230
|
+
if (last >= 0 && typeof acc.parts[last] === "string")
|
|
231
|
+
acc.parts[last] += s;
|
|
232
|
+
else
|
|
233
|
+
acc.parts.push(s);
|
|
234
|
+
if (this.rec)
|
|
235
|
+
for (const id of this.rec.open)
|
|
236
|
+
this.rec.anchors.set(id, (this.rec.anchors.get(id) ?? "") + s);
|
|
237
|
+
}
|
|
238
|
+
run(r, acc) {
|
|
239
|
+
const reasons = hiddenReasons(this.styles.run(child(r, "rPr"), this.paragraphStyle));
|
|
240
|
+
for (let c = r.firstChild; c; c = c.nextSibling) {
|
|
241
|
+
if (!isW(c)) {
|
|
242
|
+
if (c.nodeType === 1 && /txbxContent|AlternateContent/.test(c.localName ?? "") && this.rec)
|
|
243
|
+
this.rec.textBoxes = true;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
let s = "";
|
|
247
|
+
if (c.localName === "t")
|
|
248
|
+
s = c.textContent ?? "";
|
|
249
|
+
else if (c.localName === "tab" || c.localName === "ptab")
|
|
250
|
+
s = "\t";
|
|
251
|
+
else if (c.localName === "br" || c.localName === "cr")
|
|
252
|
+
s = "\n";
|
|
253
|
+
else if (c.localName === "noBreakHyphen")
|
|
254
|
+
s = "-";
|
|
255
|
+
else if ((c.localName === "drawing" || c.localName === "pict") && this.rec) {
|
|
256
|
+
if (c.getElementsByTagNameNS(W, "txbxContent").length)
|
|
257
|
+
this.rec.textBoxes = true;
|
|
258
|
+
}
|
|
259
|
+
if (!s)
|
|
260
|
+
continue;
|
|
261
|
+
if (reasons.length) {
|
|
262
|
+
if (this.rec && this.hiddenHere) {
|
|
263
|
+
this.hiddenHere.text += s;
|
|
264
|
+
reasons.forEach((x) => this.hiddenHere.reasons.add(x));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else
|
|
268
|
+
this.visible(acc, s);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
inline(node, acc) {
|
|
272
|
+
for (let c = node.firstChild; c; c = c.nextSibling) {
|
|
273
|
+
if (!isW(c))
|
|
274
|
+
continue;
|
|
275
|
+
switch (c.localName) {
|
|
276
|
+
case "r":
|
|
277
|
+
this.run(c, acc);
|
|
278
|
+
break;
|
|
279
|
+
case "del":
|
|
280
|
+
case "moveFrom":
|
|
281
|
+
if (this.rec)
|
|
282
|
+
this.rec.deletions++;
|
|
283
|
+
break;
|
|
284
|
+
case "ins":
|
|
285
|
+
case "moveTo":
|
|
286
|
+
if (this.rec)
|
|
287
|
+
this.rec.insertions++;
|
|
288
|
+
this.inline(c, acc);
|
|
289
|
+
break;
|
|
290
|
+
case "sdt": {
|
|
291
|
+
const content = contentOf(c);
|
|
292
|
+
if (!content)
|
|
293
|
+
break;
|
|
294
|
+
const tag = tagOf(c);
|
|
295
|
+
if (tag)
|
|
296
|
+
this.checkTag(tag);
|
|
297
|
+
const sub = { plain: "", parts: [] };
|
|
298
|
+
this.inline(content, sub);
|
|
299
|
+
if (tag && this.rec)
|
|
300
|
+
this.field(c, tag, sub.plain);
|
|
301
|
+
acc.plain += sub.plain;
|
|
302
|
+
if (tag && this.marks)
|
|
303
|
+
acc.parts.push({ mark: MARK_OPEN(tag) }, ...sub.parts, { mark: MARK_CLOSE });
|
|
304
|
+
else
|
|
305
|
+
for (const p of sub.parts)
|
|
306
|
+
typeof p === "string" ? this.append(acc, p) : acc.parts.push(p);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
case "commentRangeStart": {
|
|
310
|
+
const id = c.getAttributeNS(W, "id");
|
|
311
|
+
if (id && this.rec)
|
|
312
|
+
this.rec.open.add(id);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case "commentRangeEnd": {
|
|
316
|
+
const id = c.getAttributeNS(W, "id");
|
|
317
|
+
if (id && this.rec)
|
|
318
|
+
this.rec.open.delete(id);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "hyperlink":
|
|
322
|
+
case "smartTag":
|
|
323
|
+
case "customXml":
|
|
324
|
+
case "fldSimple":
|
|
325
|
+
case "bdo":
|
|
326
|
+
case "dir":
|
|
327
|
+
this.inline(c, acc);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
append(acc, s) {
|
|
333
|
+
const last = acc.parts.length - 1;
|
|
334
|
+
if (last >= 0 && typeof acc.parts[last] === "string")
|
|
335
|
+
acc.parts[last] += s;
|
|
336
|
+
else
|
|
337
|
+
acc.parts.push(s);
|
|
338
|
+
}
|
|
339
|
+
field(sdt, tag, inlineValue) {
|
|
340
|
+
const rec = this.rec;
|
|
341
|
+
const seen = rec.fields.get(tag);
|
|
342
|
+
if (seen) {
|
|
343
|
+
seen.repeated++;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const pr = child(sdt, "sdtPr");
|
|
347
|
+
const table = fieldTable(sdt);
|
|
348
|
+
const content = contentOf(sdt);
|
|
349
|
+
const quiet = new Reader(false, null, this.styles);
|
|
350
|
+
const value = inlineValue ?? (content ? quiet.lines(content, fieldPlace(sdt)).join("\n") : "");
|
|
351
|
+
rec.fields.set(tag, {
|
|
352
|
+
tag,
|
|
353
|
+
title: pr ? val(child(pr, "alias")) : null,
|
|
354
|
+
kind: table ? "table" : "text",
|
|
355
|
+
value,
|
|
356
|
+
...(table ? { rows: quiet.tableRows(table) } : {}),
|
|
357
|
+
placeholder: pr ? on(child(pr, "showingPlcHdr")) : false,
|
|
358
|
+
repeated: 1,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
/** One paragraph's visible text, wrapped in the markers of the block fields around it. */
|
|
362
|
+
paragraph(p, where) {
|
|
363
|
+
const acc = { plain: "", parts: [] };
|
|
364
|
+
const outer = this.hiddenHere;
|
|
365
|
+
const outerStyle = this.paragraphStyle;
|
|
366
|
+
const pPr = child(p, "pPr");
|
|
367
|
+
this.paragraphStyle = pPr ? val(child(pPr, "pStyle")) : null;
|
|
368
|
+
this.hiddenHere = { text: "", reasons: new Set() };
|
|
369
|
+
this.inline(p, acc);
|
|
370
|
+
if (this.rec && this.hiddenHere.text)
|
|
371
|
+
this.rec.hidden.push({
|
|
372
|
+
where,
|
|
373
|
+
reasons: [...this.hiddenHere.reasons],
|
|
374
|
+
text: this.hiddenHere.text,
|
|
375
|
+
});
|
|
376
|
+
this.hiddenHere = outer;
|
|
377
|
+
this.paragraphStyle = outerStyle;
|
|
378
|
+
let text = this.marks ? this.render(acc.parts) : acc.plain;
|
|
379
|
+
if (this.marks)
|
|
380
|
+
for (const tag of [...this.blockTags].reverse())
|
|
381
|
+
text = `${MARK_OPEN(tag)}${text}${MARK_CLOSE}`;
|
|
382
|
+
return text;
|
|
383
|
+
}
|
|
384
|
+
/** Block content (a cell, a header, a field's content) as lines, nested tables flattened. */
|
|
385
|
+
lines(node, place = "block", where = "body") {
|
|
386
|
+
if (place === "row")
|
|
387
|
+
return this.tableRowsOf(node, where).map((r) => r.join("\t"));
|
|
388
|
+
if (place === "cell")
|
|
389
|
+
return this.cellsOf(node, where);
|
|
390
|
+
if (place === "inline") {
|
|
391
|
+
const acc = { plain: "", parts: [] };
|
|
392
|
+
this.inline(node, acc);
|
|
393
|
+
return [this.marks ? this.render(acc.parts) : acc.plain];
|
|
394
|
+
}
|
|
395
|
+
const out = [];
|
|
396
|
+
for (let c = node.firstChild; c; c = c.nextSibling) {
|
|
397
|
+
if (!isW(c))
|
|
398
|
+
continue;
|
|
399
|
+
if (c.localName === "p")
|
|
400
|
+
out.push(this.paragraph(c, where));
|
|
401
|
+
else if (c.localName === "tbl")
|
|
402
|
+
out.push(...this.tableRows(c, where).map((r) => r.join("\t")));
|
|
403
|
+
else if (c.localName === "sdt")
|
|
404
|
+
this.blockField(c, () => out.push(...this.lines(contentOf(c), "block", where)));
|
|
405
|
+
else if (c.localName === "customXml")
|
|
406
|
+
out.push(...this.lines(c, "block", where));
|
|
407
|
+
else if (c.localName === "ins" || c.localName === "moveTo") {
|
|
408
|
+
if (this.rec)
|
|
409
|
+
this.rec.insertions++;
|
|
410
|
+
out.push(...this.lines(c, "block", where));
|
|
411
|
+
}
|
|
412
|
+
else if ((c.localName === "del" || c.localName === "moveFrom") && this.rec)
|
|
413
|
+
this.rec.deletions++;
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
blockField(sdt, read) {
|
|
418
|
+
const content = contentOf(sdt);
|
|
419
|
+
if (!content)
|
|
420
|
+
return;
|
|
421
|
+
const tag = tagOf(sdt);
|
|
422
|
+
if (tag)
|
|
423
|
+
this.checkTag(tag);
|
|
424
|
+
if (tag && this.rec)
|
|
425
|
+
this.field(sdt, tag);
|
|
426
|
+
if (tag)
|
|
427
|
+
this.blockTags.push(tag);
|
|
428
|
+
try {
|
|
429
|
+
read();
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
if (tag)
|
|
433
|
+
this.blockTags.pop();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
cellsOf(node, where, cells = []) {
|
|
437
|
+
for (let c = node.firstChild; c; c = c.nextSibling) {
|
|
438
|
+
if (cells.length > MAX_ROW_CELLS)
|
|
439
|
+
throw new Denied(`That file has a table with more than ${MAX_ROW_CELLS} cells in a row, so it is not read`);
|
|
440
|
+
if (isW(c, "tc"))
|
|
441
|
+
cells.push(this.lines(c, "block", where).join("\n"));
|
|
442
|
+
else if (isW(c, "sdt"))
|
|
443
|
+
this.blockField(c, () => this.cellsOf(contentOf(c), where, cells));
|
|
444
|
+
else if (isW(c, "customXml"))
|
|
445
|
+
this.cellsOf(c, where, cells);
|
|
446
|
+
}
|
|
447
|
+
if (cells.length > MAX_ROW_CELLS)
|
|
448
|
+
throw new Denied(`That file has a table with more than ${MAX_ROW_CELLS} cells in a row, so it is not read`);
|
|
449
|
+
return cells;
|
|
450
|
+
}
|
|
451
|
+
tableRowsOf(node, where, rows = []) {
|
|
452
|
+
for (let c = node.firstChild; c; c = c.nextSibling) {
|
|
453
|
+
if (rows.length > MAX_TABLE_ROWS)
|
|
454
|
+
throw new Denied(`That file has a table with more than ${MAX_TABLE_ROWS} rows, so it is not read`);
|
|
455
|
+
if (isW(c, "tr"))
|
|
456
|
+
rows.push(this.cellsOf(c, where));
|
|
457
|
+
else if (isW(c, "sdt"))
|
|
458
|
+
this.blockField(c, () => this.tableRowsOf(contentOf(c), where, rows));
|
|
459
|
+
else if (isW(c, "customXml"))
|
|
460
|
+
this.tableRowsOf(c, where, rows);
|
|
461
|
+
}
|
|
462
|
+
return rows;
|
|
463
|
+
}
|
|
464
|
+
tableRows(tbl, where = "body") {
|
|
465
|
+
return this.tableRowsOf(tbl, where);
|
|
466
|
+
}
|
|
467
|
+
/** The document body: paragraphs and tables in reading order. */
|
|
468
|
+
body(node, out) {
|
|
469
|
+
for (let c = node.firstChild; c; c = c.nextSibling) {
|
|
470
|
+
if (!isW(c))
|
|
471
|
+
continue;
|
|
472
|
+
if (c.localName === "p") {
|
|
473
|
+
const i = out.paragraphs.length;
|
|
474
|
+
const pPr = child(c, "pPr");
|
|
475
|
+
const text = this.paragraph(c, `paragraph ${i}`);
|
|
476
|
+
out.paragraphs.push({ i, style: pPr ? val(child(pPr, "pStyle")) : null, text, in_table: false });
|
|
477
|
+
out.order.push(["paragraphs", i]);
|
|
478
|
+
}
|
|
479
|
+
else if (c.localName === "tbl") {
|
|
480
|
+
const i = out.tables.length;
|
|
481
|
+
const after = out.paragraphs.length ? out.paragraphs.length - 1 : null;
|
|
482
|
+
out.tables.push({ i, after_paragraph: after, rows: this.tableRows(c, `table ${i}`) });
|
|
483
|
+
out.order.push(["tables", i]);
|
|
484
|
+
}
|
|
485
|
+
else if (c.localName === "sdt")
|
|
486
|
+
this.blockField(c, () => this.body(contentOf(c), out));
|
|
487
|
+
else if (c.localName === "customXml")
|
|
488
|
+
this.body(c, out);
|
|
489
|
+
else if (c.localName === "ins" || c.localName === "moveTo") {
|
|
490
|
+
if (this.rec)
|
|
491
|
+
this.rec.insertions++;
|
|
492
|
+
this.body(c, out);
|
|
493
|
+
}
|
|
494
|
+
else if ((c.localName === "del" || c.localName === "moveFrom") && this.rec)
|
|
495
|
+
this.rec.deletions++;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const bodyOf = (doc) => {
|
|
500
|
+
const body = doc.documentElement && isW(doc.documentElement, "document") ? child(doc.documentElement, "body") : null;
|
|
501
|
+
if (!body)
|
|
502
|
+
throw new Denied("That file is not a Word document (it has no document body)");
|
|
503
|
+
return body;
|
|
504
|
+
};
|
|
505
|
+
/** The parts of a Word file needed to read it, or (for filling) all of them. */
|
|
506
|
+
export function openDocx(bytes, all = false) {
|
|
507
|
+
const parts = openZip(bytes, all ? undefined : READ_PARTS);
|
|
508
|
+
if (!parts.has(DOCUMENT))
|
|
509
|
+
throw new Denied("That file is not a Word document (no word/document.xml)");
|
|
510
|
+
return parts;
|
|
511
|
+
}
|
|
512
|
+
export function readDocx(bytes, opts = {}) {
|
|
513
|
+
const parts = openDocx(bytes);
|
|
514
|
+
const styles = parts.has(STYLES) ? new Styles(parseXml(parts.get(STYLES), STYLES)) : NO_STYLES;
|
|
515
|
+
const rec = new Recorder();
|
|
516
|
+
const reader = new Reader(Boolean(opts.markFields), rec, styles);
|
|
517
|
+
const out = {
|
|
518
|
+
paragraphs: [],
|
|
519
|
+
tables: [],
|
|
520
|
+
order: [],
|
|
521
|
+
fields: [],
|
|
522
|
+
hidden: [],
|
|
523
|
+
headers_footers: [],
|
|
524
|
+
comments: [],
|
|
525
|
+
tracked_changes: { insertions: 0, deletions: 0 },
|
|
526
|
+
notes: [],
|
|
527
|
+
};
|
|
528
|
+
reader.body(bodyOf(parseXml(parts.get(DOCUMENT), DOCUMENT)), out);
|
|
529
|
+
const headerParts = [...parts.keys()]
|
|
530
|
+
.map((name) => ({ name, m: HEADER_FOOTER.exec(name) }))
|
|
531
|
+
.filter((p) => p.m)
|
|
532
|
+
.sort((a, b) => a.m[1].localeCompare(b.m[1]) || Number(a.m[2] || 0) - Number(b.m[2] || 0));
|
|
533
|
+
const headerRec = new Recorder();
|
|
534
|
+
for (const { name, m } of headerParts) {
|
|
535
|
+
const part = `${m[1]}${m[2]}`;
|
|
536
|
+
const root = parseXml(parts.get(name), name).documentElement;
|
|
537
|
+
const text = new Reader(false, headerRec, styles).lines(root, "block", part).join("\n");
|
|
538
|
+
out.headers_footers.push({ part, text });
|
|
539
|
+
}
|
|
540
|
+
const comments = parts.get("word/comments.xml");
|
|
541
|
+
if (comments) {
|
|
542
|
+
const root = parseXml(comments, "word/comments.xml").documentElement;
|
|
543
|
+
for (const c of children(root, "comment")) {
|
|
544
|
+
const id = c.getAttributeNS(W, "id") ?? "";
|
|
545
|
+
out.comments.push({
|
|
546
|
+
author: c.getAttributeNS(W, "author") || null,
|
|
547
|
+
text: new Reader(false, null, styles).lines(c).join("\n"),
|
|
548
|
+
anchor: (rec.anchors.get(id) ?? "").slice(0, 500),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
out.fields = [...rec.fields.values()];
|
|
553
|
+
out.hidden = [...rec.hidden, ...headerRec.hidden];
|
|
554
|
+
out.tracked_changes = { insertions: rec.insertions, deletions: rec.deletions };
|
|
555
|
+
if (rec.insertions || rec.deletions)
|
|
556
|
+
out.notes.push("Tracked insertions are shown in the text; tracked deletions are left out.");
|
|
557
|
+
if (rec.textBoxes || headerRec.textBoxes)
|
|
558
|
+
out.notes.push("Text in text boxes and drawings is not read.");
|
|
559
|
+
if (rec.neutralized)
|
|
560
|
+
out.notes.push("The document's own text contains [[: shown as [ [ so it cannot pass for a field marker.");
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
// eslint-disable-next-line no-control-regex -- control characters are what it refuses
|
|
564
|
+
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/;
|
|
565
|
+
const allFields = (doc) => Array.from(doc.getElementsByTagNameNS(W, "sdt")).filter((s) => tagOf(s) !== null && isInBody(s));
|
|
566
|
+
function isInBody(n) {
|
|
567
|
+
for (let p = n.parentNode; p; p = p.parentNode)
|
|
568
|
+
if (isW(p, "body"))
|
|
569
|
+
return true;
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
const contains = (outer, inner) => {
|
|
573
|
+
for (let p = inner.parentNode; p; p = p.parentNode)
|
|
574
|
+
if (p === outer)
|
|
575
|
+
return true;
|
|
576
|
+
return false;
|
|
577
|
+
};
|
|
578
|
+
/** Deep copy of `el` without revision marks and section breaks. */
|
|
579
|
+
function cleanCopy(el) {
|
|
580
|
+
if (!el)
|
|
581
|
+
return null;
|
|
582
|
+
const copy = el.cloneNode(true);
|
|
583
|
+
for (const name of ["rPrChange", "pPrChange", "ins", "del", "sectPr"])
|
|
584
|
+
for (const x of Array.from(copy.getElementsByTagNameNS(W, name)))
|
|
585
|
+
x.parentNode?.removeChild(x);
|
|
586
|
+
return copy;
|
|
587
|
+
}
|
|
588
|
+
const firstRun = (node) => Array.from(node.getElementsByTagNameNS(W, "r")).find((r) => !r.parentNode || !(isW(r.parentNode, "del") || isW(r.parentNode, "moveFrom"))) ?? null;
|
|
589
|
+
// rPr children that come after w:highlight in the schema's sequence (CT_RPr).
|
|
590
|
+
const AFTER_HIGHLIGHT = new Set([
|
|
591
|
+
"u", "effect", "bdr", "shd", "fitText", "vertAlign", "rtl", "cs", "em", "lang",
|
|
592
|
+
"eastAsianLayout", "specVanish", "oMath",
|
|
593
|
+
]);
|
|
594
|
+
function runProps(doc, template, highlight) {
|
|
595
|
+
let rPr = cleanCopy(template ? child(template, "rPr") : null);
|
|
596
|
+
// Word's "click here" text is styled PlaceholderText; typed text does not keep it.
|
|
597
|
+
const style = rPr ? child(rPr, "rStyle") : null;
|
|
598
|
+
if (style && /^placeholder\s*text$/i.test(val(style) ?? ""))
|
|
599
|
+
rPr.removeChild(style);
|
|
600
|
+
if (highlight) {
|
|
601
|
+
rPr ??= doc.createElementNS(W, "w:rPr");
|
|
602
|
+
const old = child(rPr, "highlight");
|
|
603
|
+
if (old)
|
|
604
|
+
rPr.removeChild(old);
|
|
605
|
+
const mark = doc.createElementNS(W, "w:highlight");
|
|
606
|
+
mark.setAttributeNS(W, "w:val", "yellow");
|
|
607
|
+
const before = children(rPr).find((c) => AFTER_HIGHLIGHT.has(c.localName ?? ""));
|
|
608
|
+
rPr.insertBefore(mark, before ?? null);
|
|
609
|
+
}
|
|
610
|
+
return rPr && rPr.firstChild ? rPr : null;
|
|
611
|
+
}
|
|
612
|
+
/** Runs for one line of text: text, with tabs as w:tab. */
|
|
613
|
+
function runsFor(doc, line, rPr) {
|
|
614
|
+
if (!line)
|
|
615
|
+
return [];
|
|
616
|
+
const r = doc.createElementNS(W, "w:r");
|
|
617
|
+
if (rPr)
|
|
618
|
+
r.appendChild(rPr.cloneNode(true));
|
|
619
|
+
line.split("\t").forEach((piece, i) => {
|
|
620
|
+
if (i)
|
|
621
|
+
r.appendChild(doc.createElementNS(W, "w:tab"));
|
|
622
|
+
if (!piece)
|
|
623
|
+
return;
|
|
624
|
+
const t = doc.createElementNS(W, "w:t");
|
|
625
|
+
t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
|
|
626
|
+
t.appendChild(doc.createTextNode(piece));
|
|
627
|
+
r.appendChild(t);
|
|
628
|
+
});
|
|
629
|
+
return [r];
|
|
630
|
+
}
|
|
631
|
+
function paragraphs(doc, text, pPr, rPr) {
|
|
632
|
+
return text.split("\n").map((line) => {
|
|
633
|
+
const p = doc.createElementNS(W, "w:p");
|
|
634
|
+
if (pPr)
|
|
635
|
+
p.appendChild(pPr.cloneNode(true));
|
|
636
|
+
for (const r of runsFor(doc, line, rPr))
|
|
637
|
+
p.appendChild(r);
|
|
638
|
+
return p;
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
const clear = (node, keep = () => false) => {
|
|
642
|
+
for (const c of Array.from(node.childNodes))
|
|
643
|
+
if (!keep(c))
|
|
644
|
+
node.removeChild(c);
|
|
645
|
+
};
|
|
646
|
+
function fillText(doc, sdt, text, highlight) {
|
|
647
|
+
const content = contentOf(sdt);
|
|
648
|
+
const run = firstRun(content);
|
|
649
|
+
const rPr = runProps(doc, run, highlight);
|
|
650
|
+
if (fieldPlace(sdt) === "inline") {
|
|
651
|
+
clear(content);
|
|
652
|
+
text.split("\n").forEach((line, i) => {
|
|
653
|
+
if (i) {
|
|
654
|
+
const br = doc.createElementNS(W, "w:r");
|
|
655
|
+
if (rPr)
|
|
656
|
+
br.appendChild(rPr.cloneNode(true));
|
|
657
|
+
br.appendChild(doc.createElementNS(W, "w:br"));
|
|
658
|
+
content.appendChild(br);
|
|
659
|
+
}
|
|
660
|
+
for (const r of runsFor(doc, line, rPr))
|
|
661
|
+
content.appendChild(r);
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const p = content.getElementsByTagNameNS(W, "p")[0] ?? null;
|
|
666
|
+
const pPr = cleanCopy(p ? child(p, "pPr") : null);
|
|
667
|
+
clear(content);
|
|
668
|
+
for (const para of paragraphs(doc, text, pPr && pPr.firstChild ? pPr : null, rPr))
|
|
669
|
+
content.appendChild(para);
|
|
670
|
+
}
|
|
671
|
+
function setCell(doc, tc, text) {
|
|
672
|
+
const p = child(tc, "p");
|
|
673
|
+
const pPr = cleanCopy(p ? child(p, "pPr") : null);
|
|
674
|
+
const rPr = runProps(doc, p ? firstRun(p) : null, false);
|
|
675
|
+
clear(tc, (c) => isW(c, "tcPr"));
|
|
676
|
+
for (const para of paragraphs(doc, text, pPr && pPr.firstChild ? pPr : null, rPr))
|
|
677
|
+
tc.appendChild(para);
|
|
678
|
+
}
|
|
679
|
+
const ROW_KEEP = new Set(["trPr", "tblPrEx", "tc"]);
|
|
680
|
+
function fillTable(doc, sdt, rows) {
|
|
681
|
+
const tbl = fieldTable(sdt);
|
|
682
|
+
const trs = children(tbl, "tr");
|
|
683
|
+
const template = trs.at(-1);
|
|
684
|
+
for (const tr of trs.slice(1))
|
|
685
|
+
tbl.removeChild(tr);
|
|
686
|
+
for (const values of rows) {
|
|
687
|
+
const tr = template.cloneNode(true);
|
|
688
|
+
clear(tr, (c) => isW(c) && ROW_KEEP.has(c.localName ?? ""));
|
|
689
|
+
children(tr, "tc").forEach((tc, i) => setCell(doc, tc, values[i] ?? ""));
|
|
690
|
+
tbl.appendChild(tr);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/** Clears Word's placeholder flag and any data binding, which would overwrite the value. */
|
|
694
|
+
function settle(sdt) {
|
|
695
|
+
const pr = child(sdt, "sdtPr");
|
|
696
|
+
if (!pr)
|
|
697
|
+
return;
|
|
698
|
+
for (const c of Array.from(pr.childNodes))
|
|
699
|
+
if (c.nodeType === 1 && (isW(c, "showingPlcHdr") || c.localName === "dataBinding"))
|
|
700
|
+
pr.removeChild(c);
|
|
701
|
+
}
|
|
702
|
+
function plan(doc, fields) {
|
|
703
|
+
const tags = Object.keys(fields);
|
|
704
|
+
if (tags.length > MAX_TAGS)
|
|
705
|
+
throw new Denied(`Fill at most ${MAX_TAGS} fields at a time`);
|
|
706
|
+
const found = allFields(doc);
|
|
707
|
+
const byTag = new Map();
|
|
708
|
+
for (const s of found)
|
|
709
|
+
byTag.set(tagOf(s), [...(byTag.get(tagOf(s)) ?? []), s]);
|
|
710
|
+
const plans = [];
|
|
711
|
+
for (const tag of tags) {
|
|
712
|
+
const name = clip(tag, 100);
|
|
713
|
+
const sdts = byTag.get(tag);
|
|
714
|
+
if (!sdts)
|
|
715
|
+
throw new Denied(`${name} is not a field in the template`);
|
|
716
|
+
const value = fields[tag];
|
|
717
|
+
const texts = typeof value === "string" ? [value] : "rows" in value ? value.rows.flat() : [value.text];
|
|
718
|
+
for (const t of texts) {
|
|
719
|
+
if (t.length > MAX_TEXT)
|
|
720
|
+
throw new Denied(`The value for ${name} is longer than ${MAX_TEXT} characters`);
|
|
721
|
+
if (CONTROL.test(t))
|
|
722
|
+
throw new Denied(`The value for ${name} has control characters`);
|
|
723
|
+
}
|
|
724
|
+
for (const sdt of sdts) {
|
|
725
|
+
const place = fieldPlace(sdt);
|
|
726
|
+
if (place === "row" || place === "cell")
|
|
727
|
+
throw new Denied(`${name} wraps table rows or cells, which cannot be filled here`);
|
|
728
|
+
if (!contentOf(sdt))
|
|
729
|
+
throw new Denied(`${name} has no content to replace`);
|
|
730
|
+
const table = fieldTable(sdt);
|
|
731
|
+
if (typeof value === "object" && "rows" in value) {
|
|
732
|
+
if (!table)
|
|
733
|
+
throw new Denied(`${name} is a text field: give it text, not rows`);
|
|
734
|
+
const trs = children(table, "tr");
|
|
735
|
+
if (trs.length < 2 || children(table).some((c) => ["sdt", "customXml"].includes(c.localName ?? "")))
|
|
736
|
+
throw new Denied(`${name}'s table needs a header row and a row to repeat`);
|
|
737
|
+
if (value.rows.length > MAX_ROWS)
|
|
738
|
+
throw new Denied(`Give ${name} at most ${MAX_ROWS} rows`);
|
|
739
|
+
const columns = children(trs.at(-1), "tc").length;
|
|
740
|
+
for (const row of value.rows) {
|
|
741
|
+
if (row.length > MAX_CELLS)
|
|
742
|
+
throw new Denied(`A row for ${name} has ${row.length} cells; at most ${MAX_CELLS}`);
|
|
743
|
+
if (row.length > columns)
|
|
744
|
+
throw new Denied(`A row for ${name} has ${row.length} cells; its table has ${columns} columns`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (table && !(typeof value === "string" && value === ""))
|
|
748
|
+
throw new Denied(`${name} is a table field: give it {"rows": [...]}`);
|
|
749
|
+
else if (contentOf(sdt).getElementsByTagNameNS(W, "sectPr").length)
|
|
750
|
+
throw new Denied(`${name} holds a section break, which cannot be replaced`);
|
|
751
|
+
plans.push({ sdt, tag, value });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
for (const a of plans)
|
|
755
|
+
for (const b of plans)
|
|
756
|
+
if (a !== b && contains(a.sdt, b.sdt))
|
|
757
|
+
throw new Denied(`${clip(b.tag, 100)} is inside ${clip(a.tag, 100)}: fill one of them, not both`);
|
|
758
|
+
const unfilled = [...new Set(found.map((s) => tagOf(s)))].filter((t) => !(t in fields));
|
|
759
|
+
return { plans, unfilled };
|
|
760
|
+
}
|
|
761
|
+
/** The document with these fields' content taken out and their flags settled: what a fill must
|
|
762
|
+
* not change, text and markup alike. The content is put back afterwards. */
|
|
763
|
+
function skeleton(doc, sdts) {
|
|
764
|
+
const taken = sdts.map((sdt) => {
|
|
765
|
+
settle(sdt);
|
|
766
|
+
const content = contentOf(sdt);
|
|
767
|
+
const kids = content ? Array.from(content.childNodes) : [];
|
|
768
|
+
for (const k of kids)
|
|
769
|
+
content.removeChild(k);
|
|
770
|
+
return { content, kids };
|
|
771
|
+
});
|
|
772
|
+
const text = new TextDecoder().decode(serializeXml(doc));
|
|
773
|
+
for (const { content, kids } of taken)
|
|
774
|
+
for (const k of kids)
|
|
775
|
+
content.appendChild(k);
|
|
776
|
+
return text;
|
|
777
|
+
}
|
|
778
|
+
const same = (a, b) => a.length === b.length && Buffer.from(a.buffer, a.byteOffset, a.length).equals(Buffer.from(b.buffer, b.byteOffset, b.length));
|
|
779
|
+
/** Checks a filled file against its template before it may be uploaded. Throws Denied. The
|
|
780
|
+
* output's document.xml is parsed once more here: the check is on the bytes to be uploaded. */
|
|
781
|
+
export function guard(template, before, output, tags) {
|
|
782
|
+
const after = openDocx(output, true);
|
|
783
|
+
const names = [...template.keys()];
|
|
784
|
+
if (names.length !== after.size || names.some((n) => !after.has(n)))
|
|
785
|
+
throw new Denied("The filled file's parts differ from the template's, so it was not saved (a bug)");
|
|
786
|
+
for (const name of names)
|
|
787
|
+
if (name !== DOCUMENT && !same(template.get(name), after.get(name)))
|
|
788
|
+
throw new Denied(`The fill changed ${clip(name, 100)}, so it was not saved (a bug)`);
|
|
789
|
+
const doc = parseXml(after.get(DOCUMENT), DOCUMENT);
|
|
790
|
+
if (before !== skeleton(doc, allFields(doc).filter((s) => tags.has(tagOf(s)))))
|
|
791
|
+
throw new Denied("The fill changed text outside the fields, so it was not saved (a bug)");
|
|
792
|
+
}
|
|
793
|
+
// The template parsed once: planned, its skeleton taken, filled and serialized. Separate from
|
|
794
|
+
// the guard so the template's DOM can be collected before the output is parsed.
|
|
795
|
+
function fillParts(parts, fields, sabotage) {
|
|
796
|
+
const doc = parseXml(parts.get(DOCUMENT), DOCUMENT);
|
|
797
|
+
bodyOf(doc);
|
|
798
|
+
const { plans, unfilled } = plan(doc, fields);
|
|
799
|
+
const before = skeleton(doc, plans.map((p) => p.sdt));
|
|
800
|
+
for (const { sdt, value } of plans) {
|
|
801
|
+
if (typeof value === "object" && "rows" in value)
|
|
802
|
+
fillTable(doc, sdt, value.rows);
|
|
803
|
+
else if (fieldTable(sdt))
|
|
804
|
+
fillTable(doc, sdt, []);
|
|
805
|
+
else if (typeof value === "string")
|
|
806
|
+
fillText(doc, sdt, value.replace(/\r\n?/g, "\n"), false);
|
|
807
|
+
else
|
|
808
|
+
fillText(doc, sdt, value.text.replace(/\r\n?/g, "\n"), Boolean(value.highlight));
|
|
809
|
+
settle(sdt);
|
|
810
|
+
}
|
|
811
|
+
const out = new Map(parts);
|
|
812
|
+
sabotage?.(doc, out);
|
|
813
|
+
out.set(DOCUMENT, serializeXml(doc));
|
|
814
|
+
return { bytes: makeZip(out), before, unfilled };
|
|
815
|
+
}
|
|
816
|
+
/** A new Word file from `template` with `fields` filled. `sabotage` is for the guard's test. */
|
|
817
|
+
export function fillDocx(template, fields, sabotage) {
|
|
818
|
+
const parts = openDocx(template, true);
|
|
819
|
+
const { bytes, before, unfilled } = fillParts(parts, fields, sabotage);
|
|
820
|
+
guard(parts, before, bytes, new Set(Object.keys(fields)));
|
|
821
|
+
return { bytes, filled: Object.keys(fields), unfilled };
|
|
822
|
+
}
|
|
823
|
+
//# sourceMappingURL=docx.js.map
|