@aglyn/aglyn 1.0.0-beta.197 → 1.0.0-beta.199
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/package.json +11 -11
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +3 -3
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
- package/src/lib/app-utils/media-embedded-fields.js +619 -0
- package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +12 -0
- package/src/lib/app-utils/name-search.js +11 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/server.d.ts +1 -0
- package/src/server.js +7 -0
- package/src/server.js.map +1 -1
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* The document properties of a Word, Excel or PowerPoint file (AGL-3331):
|
|
19
|
+
* read from, and written back into, the package's property parts.
|
|
20
|
+
*
|
|
21
|
+
* An Office file is an Open Packaging Conventions package — a ZIP whose
|
|
22
|
+
* parts are found through relationships (ECMA-376 Part 2). Three parts carry
|
|
23
|
+
* what Explorer, Finder and Office's own Info pane show:
|
|
24
|
+
*
|
|
25
|
+
* - core properties (ECMA-376 Part 2, "Core Properties"): title, subject,
|
|
26
|
+
* author, keywords, comments, category, last modified by, the two dates.
|
|
27
|
+
* Readable and writable.
|
|
28
|
+
* - extended properties (Part 1 §22.2): the application, the company, the
|
|
29
|
+
* page or slide count. Read-only — the application recomputes them.
|
|
30
|
+
* - custom properties (Part 1 §22.3): named, typed values. Read; a text one
|
|
31
|
+
* can be edited or removed, not added.
|
|
32
|
+
*
|
|
33
|
+
* Each part is found through the package relationships in `_rels/.rels`,
|
|
34
|
+
* never by its usual name — `docProps/core.xml` is a convention, not a rule.
|
|
35
|
+
*
|
|
36
|
+
* ## Lossless writing
|
|
37
|
+
*
|
|
38
|
+
* An edited part is changed as TEXT at the exact offsets the parser
|
|
39
|
+
* recorded: one element's content replaced, one element removed, one
|
|
40
|
+
* inserted before the root's end tag. Every other byte of the part stays as
|
|
41
|
+
* it was, and every other part of the package is copied through byte for
|
|
42
|
+
* byte by `rewriteZip` — the document, the images, the styles, the
|
|
43
|
+
* signatures' parts.
|
|
44
|
+
*
|
|
45
|
+
* ## The XML parser
|
|
46
|
+
*
|
|
47
|
+
* A small, strict, non-validating one (`scanXml`, `parseXml`), because the
|
|
48
|
+
* parts are tiny and the only safe DTD is none: OPC forbids DTD declarations
|
|
49
|
+
* in package XML, so a DOCTYPE here is refused outright rather than
|
|
50
|
+
* skipped, and no entity beyond the five predefined ones and character
|
|
51
|
+
* references is ever expanded. `svg.ts` reuses it, allowing an inert
|
|
52
|
+
* external-id DOCTYPE and nothing more.
|
|
53
|
+
*/ import { embeddedFieldLabel, otherEmbeddedKey, parseOtherEmbeddedKey } from "../media-embedded-fields.js";
|
|
54
|
+
import { findZipEntry, readZip, readZipEntry, rewriteZip } from "./zip.js";
|
|
55
|
+
import { EmbeddedWriteError } from "./types.js";
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// XML
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
const NS_XML = 'http://www.w3.org/XML/1998/namespace';
|
|
60
|
+
const NS_XMLNS = 'http://www.w3.org/2000/xmlns/';
|
|
61
|
+
const XML_ENTITY = /&(?:#x([0-9a-fA-F]{1,6})|#([0-9]{1,7})|(amp|lt|gt|quot|apos));/g;
|
|
62
|
+
const PREDEFINED = {
|
|
63
|
+
amp: '&',
|
|
64
|
+
lt: '<',
|
|
65
|
+
gt: '>',
|
|
66
|
+
quot: '"',
|
|
67
|
+
apos: "'"
|
|
68
|
+
};
|
|
69
|
+
/** XML 1.0 §2.2 `Char`. */ function isXmlChar(code) {
|
|
70
|
+
return code === 0x9 || code === 0xa || code === 0xd || code >= 0x20 && code <= 0xd7ff || code >= 0xe000 && code <= 0xfffd || code >= 0x10000 && code <= 0x10ffff;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Expand the five predefined entities and character references (XML 1.0
|
|
74
|
+
* §4.1, §4.6). Anything else — an undeclared entity, a stray `&` — is left
|
|
75
|
+
* as written: there is no DTD to define it, and guessing is worse.
|
|
76
|
+
*/ function decodeEntities(raw) {
|
|
77
|
+
if (!raw.includes('&')) return raw;
|
|
78
|
+
return raw.replace(XML_ENTITY, (whole, hex, dec, name)=>{
|
|
79
|
+
var _PREDEFINED_name;
|
|
80
|
+
if (name) return (_PREDEFINED_name = PREDEFINED[name]) != null ? _PREDEFINED_name : whole;
|
|
81
|
+
const code = hex ? parseInt(hex, 16) : parseInt(dec != null ? dec : '', 10);
|
|
82
|
+
return isXmlChar(code) ? String.fromCodePoint(code) : whole;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Character data: line ends normalized (§2.11), then entities. */ export function decodeXmlText(raw) {
|
|
86
|
+
return decodeEntities(raw.includes('\r') ? raw.replace(/\r\n?/g, '\n') : raw);
|
|
87
|
+
}
|
|
88
|
+
/** An attribute value, normalized as §3.3.3 does for CDATA attributes. */ function decodeXmlAttribute(raw) {
|
|
89
|
+
return decodeEntities(raw.replace(/\r\n|[\t\n\r]/g, ' '));
|
|
90
|
+
}
|
|
91
|
+
/** Characters XML 1.0 cannot carry at all, even escaped. */ const NOT_XML_CHAR = // eslint-disable-next-line no-control-regex
|
|
92
|
+
/[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
|
|
93
|
+
/** Escape text content; drops what XML cannot represent. */ export function escapeXmlText(value) {
|
|
94
|
+
return value.replace(NOT_XML_CHAR, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
95
|
+
}
|
|
96
|
+
/** Escape a double-quoted attribute value. */ export function escapeXmlAttribute(value) {
|
|
97
|
+
return escapeXmlText(value).replace(/"/g, '"').replace(/\t/g, '	').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
|
98
|
+
}
|
|
99
|
+
const isSpace = (code)=>code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d;
|
|
100
|
+
/** A name runs until whitespace or markup punctuation. */ const endsName = (code)=>isSpace(code) || code === 0x2f || // /
|
|
101
|
+
code === 0x3e || // >
|
|
102
|
+
code === 0x3d || // =
|
|
103
|
+
code === 0x3c || // <
|
|
104
|
+
code === 0x22 || // "
|
|
105
|
+
code === 0x27 || // '
|
|
106
|
+
Number.isNaN(code);
|
|
107
|
+
function splitName(name) {
|
|
108
|
+
const colon = name.indexOf(':');
|
|
109
|
+
if (colon < 0) return {
|
|
110
|
+
prefix: null,
|
|
111
|
+
local: name
|
|
112
|
+
};
|
|
113
|
+
if (colon === 0 || colon === name.length - 1 || name.indexOf(':', colon + 1) >= 0) return null;
|
|
114
|
+
return {
|
|
115
|
+
prefix: name.slice(0, colon),
|
|
116
|
+
local: name.slice(colon + 1)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Walk a document start to end, reporting elements and text.
|
|
121
|
+
*
|
|
122
|
+
* Returns false — having possibly reported a prefix of the document — on
|
|
123
|
+
* anything that is not a well-formed, namespace-well-formed document with
|
|
124
|
+
* one root element: mismatched or unclosed tags, a duplicate attribute, an
|
|
125
|
+
* unbound prefix, text outside the root, a refused DOCTYPE or `<!ENTITY>`,
|
|
126
|
+
* or a bound exceeded. Never throws.
|
|
127
|
+
*/ export function scanXml(source, handlers, options = {}) {
|
|
128
|
+
var _options_maxDepth, _options_maxElements;
|
|
129
|
+
const maxDepth = (_options_maxDepth = options.maxDepth) != null ? _options_maxDepth : 256;
|
|
130
|
+
const maxElements = (_options_maxElements = options.maxElements) != null ? _options_maxElements : 1000000;
|
|
131
|
+
const baseNamespaces = new Map([
|
|
132
|
+
[
|
|
133
|
+
'xml',
|
|
134
|
+
NS_XML
|
|
135
|
+
]
|
|
136
|
+
]);
|
|
137
|
+
const stack = [];
|
|
138
|
+
const n = source.length;
|
|
139
|
+
let rootSeen = false;
|
|
140
|
+
let elements = 0;
|
|
141
|
+
// A byte-order mark decoded into the string is not content.
|
|
142
|
+
let i = source.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
143
|
+
while(i < n){
|
|
144
|
+
var _ref;
|
|
145
|
+
const lt = source.indexOf('<', i);
|
|
146
|
+
const textEnd = lt < 0 ? n : lt;
|
|
147
|
+
if (textEnd > i) {
|
|
148
|
+
if (stack.length) {
|
|
149
|
+
handlers.text == null ? void 0 : handlers.text.call(handlers, decodeXmlText(source.slice(i, textEnd)), i, textEnd);
|
|
150
|
+
} else {
|
|
151
|
+
for(let k = i; k < textEnd; k++){
|
|
152
|
+
if (!isSpace(source.charCodeAt(k))) return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (lt < 0) break;
|
|
157
|
+
if (source.startsWith('<!--', lt)) {
|
|
158
|
+
const end = source.indexOf('-->', lt + 4);
|
|
159
|
+
if (end < 0) return false;
|
|
160
|
+
i = end + 3;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (source.startsWith('<![CDATA[', lt)) {
|
|
164
|
+
if (!stack.length) return false;
|
|
165
|
+
const end = source.indexOf(']]>', lt + 9);
|
|
166
|
+
if (end < 0) return false;
|
|
167
|
+
handlers.text == null ? void 0 : handlers.text.call(handlers, source.slice(lt + 9, end), lt + 9, end);
|
|
168
|
+
i = end + 3;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (source.startsWith('<!DOCTYPE', lt)) {
|
|
172
|
+
if (!options.allowExternalDoctype || rootSeen) return false;
|
|
173
|
+
let j = lt + 9;
|
|
174
|
+
let quote = 0;
|
|
175
|
+
for(; j < n; j++){
|
|
176
|
+
const code = source.charCodeAt(j);
|
|
177
|
+
if (quote) {
|
|
178
|
+
if (code === quote) quote = 0;
|
|
179
|
+
} else if (code === 0x22 || code === 0x27) {
|
|
180
|
+
quote = code;
|
|
181
|
+
} else if (code === 0x5b) {
|
|
182
|
+
// `[` opens an internal subset: entity definitions.
|
|
183
|
+
return false;
|
|
184
|
+
} else if (code === 0x3e) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (j >= n) return false;
|
|
189
|
+
i = j + 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// `<!ENTITY`, `<!ELEMENT`, … are only legal inside a DTD.
|
|
193
|
+
if (source.startsWith('<!', lt)) return false;
|
|
194
|
+
if (source.startsWith('<?', lt)) {
|
|
195
|
+
const end = source.indexOf('?>', lt + 2);
|
|
196
|
+
if (end < 0) return false;
|
|
197
|
+
i = end + 2;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (source.startsWith('</', lt)) {
|
|
201
|
+
const gt = source.indexOf('>', lt + 2);
|
|
202
|
+
if (gt < 0) return false;
|
|
203
|
+
const open = stack.pop();
|
|
204
|
+
if (!open || open.name !== source.slice(lt + 2, gt).trimEnd()) return false;
|
|
205
|
+
handlers.close == null ? void 0 : handlers.close.call(handlers, open, lt, gt + 1);
|
|
206
|
+
i = gt + 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// A start tag.
|
|
210
|
+
if (!stack.length && rootSeen) return false;
|
|
211
|
+
if (stack.length >= maxDepth || ++elements > maxElements) return false;
|
|
212
|
+
let j = lt + 1;
|
|
213
|
+
while(j < n && !endsName(source.charCodeAt(j)))j++;
|
|
214
|
+
const name = source.slice(lt + 1, j);
|
|
215
|
+
const parts = splitName(name);
|
|
216
|
+
if (!name || !parts) return false;
|
|
217
|
+
const raw = [];
|
|
218
|
+
const seen = new Set();
|
|
219
|
+
let selfClosing = false;
|
|
220
|
+
for(;;){
|
|
221
|
+
const before = j;
|
|
222
|
+
while(j < n && isSpace(source.charCodeAt(j)))j++;
|
|
223
|
+
if (j >= n) return false;
|
|
224
|
+
const code = source.charCodeAt(j);
|
|
225
|
+
if (code === 0x3e) {
|
|
226
|
+
j++;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (code === 0x2f) {
|
|
230
|
+
if (source.charCodeAt(j + 1) !== 0x3e) return false;
|
|
231
|
+
selfClosing = true;
|
|
232
|
+
j += 2;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
// Attributes are separated from the name and each other by space.
|
|
236
|
+
if (j === before) return false;
|
|
237
|
+
const nameStart = j;
|
|
238
|
+
while(j < n && !endsName(source.charCodeAt(j)))j++;
|
|
239
|
+
const attributeName = source.slice(nameStart, j);
|
|
240
|
+
if (!attributeName || seen.has(attributeName)) return false;
|
|
241
|
+
seen.add(attributeName);
|
|
242
|
+
while(j < n && isSpace(source.charCodeAt(j)))j++;
|
|
243
|
+
if (source.charCodeAt(j) !== 0x3d) return false;
|
|
244
|
+
j++;
|
|
245
|
+
while(j < n && isSpace(source.charCodeAt(j)))j++;
|
|
246
|
+
const quote = source[j];
|
|
247
|
+
if (quote !== '"' && quote !== "'") return false;
|
|
248
|
+
const valueEnd = source.indexOf(quote, j + 1);
|
|
249
|
+
if (valueEnd < 0) return false;
|
|
250
|
+
const value = source.slice(j + 1, valueEnd);
|
|
251
|
+
if (value.includes('<')) return false;
|
|
252
|
+
raw.push({
|
|
253
|
+
name: attributeName,
|
|
254
|
+
value,
|
|
255
|
+
start: j + 1,
|
|
256
|
+
end: valueEnd
|
|
257
|
+
});
|
|
258
|
+
j = valueEnd + 1;
|
|
259
|
+
}
|
|
260
|
+
const parent = stack.at(-1);
|
|
261
|
+
const inherited = (_ref = parent == null ? void 0 : parent.namespaces) != null ? _ref : baseNamespaces;
|
|
262
|
+
let namespaces = inherited;
|
|
263
|
+
for (const attribute of raw){
|
|
264
|
+
if (attribute.name !== 'xmlns' && !attribute.name.startsWith('xmlns:')) continue;
|
|
265
|
+
if (namespaces === inherited) namespaces = new Map(inherited);
|
|
266
|
+
const prefix = attribute.name === 'xmlns' ? '' : attribute.name.slice(6);
|
|
267
|
+
const uri = decodeXmlAttribute(attribute.value);
|
|
268
|
+
const map = namespaces;
|
|
269
|
+
if (uri) map.set(prefix, uri);
|
|
270
|
+
else if (prefix) return false;
|
|
271
|
+
else map.delete('');
|
|
272
|
+
}
|
|
273
|
+
const ns = parts.prefix === null ? namespaces.get('') : namespaces.get(parts.prefix);
|
|
274
|
+
if (parts.prefix !== null && ns === undefined) return false;
|
|
275
|
+
const attributes = [];
|
|
276
|
+
for (const attribute of raw){
|
|
277
|
+
const split = splitName(attribute.name);
|
|
278
|
+
if (!split) return false;
|
|
279
|
+
let attributeNs = null;
|
|
280
|
+
if (attribute.name === 'xmlns' || split.prefix === 'xmlns') {
|
|
281
|
+
attributeNs = NS_XMLNS;
|
|
282
|
+
} else if (split.prefix !== null) {
|
|
283
|
+
const bound = namespaces.get(split.prefix);
|
|
284
|
+
if (bound === undefined) return false;
|
|
285
|
+
attributeNs = bound;
|
|
286
|
+
}
|
|
287
|
+
attributes.push({
|
|
288
|
+
name: attribute.name,
|
|
289
|
+
prefix: split.prefix,
|
|
290
|
+
local: split.local,
|
|
291
|
+
ns: attributeNs,
|
|
292
|
+
value: decodeXmlAttribute(attribute.value),
|
|
293
|
+
valueStart: attribute.start,
|
|
294
|
+
valueEnd: attribute.end
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const tag = {
|
|
298
|
+
name,
|
|
299
|
+
prefix: parts.prefix,
|
|
300
|
+
local: parts.local,
|
|
301
|
+
ns: ns != null ? ns : null,
|
|
302
|
+
attributes,
|
|
303
|
+
namespaces,
|
|
304
|
+
depth: stack.length,
|
|
305
|
+
start: lt,
|
|
306
|
+
openEnd: j,
|
|
307
|
+
selfClosing
|
|
308
|
+
};
|
|
309
|
+
rootSeen = true;
|
|
310
|
+
handlers.open == null ? void 0 : handlers.open.call(handlers, tag);
|
|
311
|
+
if (selfClosing) handlers.close == null ? void 0 : handlers.close.call(handlers, tag, j, j);
|
|
312
|
+
else stack.push(tag);
|
|
313
|
+
i = j;
|
|
314
|
+
}
|
|
315
|
+
return rootSeen && stack.length === 0;
|
|
316
|
+
}
|
|
317
|
+
/** Parse a whole document into a tree. Null when `scanXml` refuses it. */ export function parseXml(source, options = {}) {
|
|
318
|
+
const state = {
|
|
319
|
+
root: null,
|
|
320
|
+
stack: []
|
|
321
|
+
};
|
|
322
|
+
const ok = scanXml(source, {
|
|
323
|
+
open (tag) {
|
|
324
|
+
var _state_stack_at, _state, _root;
|
|
325
|
+
const parent = (_state_stack_at = state.stack.at(-1)) != null ? _state_stack_at : null;
|
|
326
|
+
const element = _extends({}, tag, {
|
|
327
|
+
parent,
|
|
328
|
+
children: [],
|
|
329
|
+
content: [],
|
|
330
|
+
closeStart: tag.openEnd,
|
|
331
|
+
end: tag.openEnd
|
|
332
|
+
});
|
|
333
|
+
parent == null ? void 0 : parent.children.push(element);
|
|
334
|
+
parent == null ? void 0 : parent.content.push(element);
|
|
335
|
+
(_root = (_state = state).root) != null ? _root : _state.root = element;
|
|
336
|
+
state.stack.push(element);
|
|
337
|
+
},
|
|
338
|
+
close (_tag, closeStart, end) {
|
|
339
|
+
const element = state.stack.pop();
|
|
340
|
+
if (!element) return;
|
|
341
|
+
element.closeStart = closeStart;
|
|
342
|
+
element.end = end;
|
|
343
|
+
},
|
|
344
|
+
text (text) {
|
|
345
|
+
var _state_stack_at;
|
|
346
|
+
(_state_stack_at = state.stack.at(-1)) == null ? void 0 : _state_stack_at.content.push(text);
|
|
347
|
+
}
|
|
348
|
+
}, options);
|
|
349
|
+
return ok ? state.root : null;
|
|
350
|
+
}
|
|
351
|
+
/** Every character of text inside an element, descendants included. */ export function xmlTextContent(element) {
|
|
352
|
+
return element.content.map((node)=>typeof node === 'string' ? node : xmlTextContent(node)).join('');
|
|
353
|
+
}
|
|
354
|
+
/** An attribute's value by local name and namespace (null = no namespace). */ export function xmlAttribute(element, local, ns = null) {
|
|
355
|
+
return element.attributes.find((attribute)=>attribute.local === local && attribute.ns === ns);
|
|
356
|
+
}
|
|
357
|
+
/** A prefix bound to `ns` in scope at `element`; `''` for the default. */ function prefixFor(element, ns) {
|
|
358
|
+
let fallback;
|
|
359
|
+
for (const [prefix, uri] of element.namespaces){
|
|
360
|
+
if (uri !== ns) continue;
|
|
361
|
+
if (prefix) return prefix;
|
|
362
|
+
fallback = prefix;
|
|
363
|
+
}
|
|
364
|
+
return fallback;
|
|
365
|
+
}
|
|
366
|
+
function applyEdits(source, edits) {
|
|
367
|
+
const ordered = [
|
|
368
|
+
...edits
|
|
369
|
+
].sort((a, b)=>b.start - a.start || b.end - a.end);
|
|
370
|
+
let out = source;
|
|
371
|
+
let floor = Infinity;
|
|
372
|
+
for (const edit of ordered){
|
|
373
|
+
if (edit.end > floor) {
|
|
374
|
+
throw new EmbeddedWriteError('The document properties could not be edited safely.');
|
|
375
|
+
}
|
|
376
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
377
|
+
floor = edit.start;
|
|
378
|
+
}
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// Package
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
const NS_CP = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties';
|
|
385
|
+
const NS_DC = 'http://purl.org/dc/elements/1.1/';
|
|
386
|
+
const NS_DCTERMS = 'http://purl.org/dc/terms/';
|
|
387
|
+
const NS_DCMITYPE = 'http://purl.org/dc/dcmitype/';
|
|
388
|
+
const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
|
|
389
|
+
/** Transitional and Strict (ISO/IEC 29500) spellings of the same idea. */ const NS_EXTENDED = new Set([
|
|
390
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
|
|
391
|
+
'http://purl.oclc.org/ooxml/officeDocument/extendedProperties'
|
|
392
|
+
]);
|
|
393
|
+
const NS_CUSTOM = new Set([
|
|
394
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties',
|
|
395
|
+
'http://purl.oclc.org/ooxml/officeDocument/customProperties'
|
|
396
|
+
]);
|
|
397
|
+
const NS_VT = new Set([
|
|
398
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes',
|
|
399
|
+
'http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes'
|
|
400
|
+
]);
|
|
401
|
+
const NS_RELATIONSHIPS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
|
402
|
+
const NS_CONTENT_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
|
403
|
+
const CONTENT_TYPES_PART = '[Content_Types].xml';
|
|
404
|
+
const ROOT_RELATIONSHIPS_PART = '_rels/.rels';
|
|
405
|
+
const CORE_RELATIONSHIP = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties';
|
|
406
|
+
const CORE_CONTENT_TYPE = 'application/vnd.openxmlformats-package.core-properties+xml';
|
|
407
|
+
const DEFAULT_CORE_PART = 'docProps/core.xml';
|
|
408
|
+
/** A property part is a few KB; this is generous and still bounded. */ const PART_MAX_BYTES = 8 * 1024 * 1024;
|
|
409
|
+
function roleOf(type) {
|
|
410
|
+
const tail = type.toLowerCase().replace(/-/g, '');
|
|
411
|
+
if (tail.endsWith('/metadata/coreproperties')) return 'core';
|
|
412
|
+
if (tail.endsWith('/extendedproperties')) return 'extended';
|
|
413
|
+
if (tail.endsWith('/customproperties')) return 'custom';
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
/** Resolve a root relationship's target to a ZIP entry name (Part 2 §9.3). */ function resolveTarget(target) {
|
|
417
|
+
var _ref;
|
|
418
|
+
var _target_split_;
|
|
419
|
+
const segments = [];
|
|
420
|
+
for (const segment of (_ref = (_target_split_ = target.split(/[?#]/)[0]) == null ? void 0 : _target_split_.split('/')) != null ? _ref : []){
|
|
421
|
+
if (!segment || segment === '.') continue;
|
|
422
|
+
if (segment === '..') segments.pop();
|
|
423
|
+
else segments.push(segment);
|
|
424
|
+
}
|
|
425
|
+
return segments.join('/');
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* A part by name. Part names compare case-insensitively (Part 2 §9.1.1.1),
|
|
429
|
+
* and a target may be percent-encoded where the ZIP item name is not.
|
|
430
|
+
*/ function findPart(archive, name) {
|
|
431
|
+
const exact = findZipEntry(archive, name);
|
|
432
|
+
if (exact) return exact;
|
|
433
|
+
let decoded = name;
|
|
434
|
+
try {
|
|
435
|
+
decoded = decodeURIComponent(name);
|
|
436
|
+
} catch (unused) {
|
|
437
|
+
// Not percent-encoded after all.
|
|
438
|
+
}
|
|
439
|
+
const wanted = [
|
|
440
|
+
name.toLowerCase(),
|
|
441
|
+
decoded.toLowerCase()
|
|
442
|
+
];
|
|
443
|
+
return archive.entries.find((entry)=>wanted.includes(entry.name.toLowerCase()));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Decode a part for reading. OPC allows UTF-8 and UTF-16 (Part 2, "XML
|
|
447
|
+
* Usage"); the byte-order mark decides, and UTF-8 is the default.
|
|
448
|
+
*/ function decodeForRead(bytes) {
|
|
449
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff) return new TextDecoder('utf-16be').decode(bytes);
|
|
450
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe) return new TextDecoder('utf-16le').decode(bytes);
|
|
451
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Decode a part for editing: strict UTF-8 only, so that re-encoding the
|
|
455
|
+
* untouched text reproduces the untouched bytes exactly.
|
|
456
|
+
*/ function decodeForEdit(bytes) {
|
|
457
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff || bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
458
|
+
throw new EmbeddedWriteError('This file stores its properties as UTF-16, which cannot be edited here.');
|
|
459
|
+
}
|
|
460
|
+
const bom = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
|
|
461
|
+
try {
|
|
462
|
+
return {
|
|
463
|
+
text: new TextDecoder('utf-8', {
|
|
464
|
+
fatal: true,
|
|
465
|
+
ignoreBOM: true
|
|
466
|
+
}).decode(bom ? bytes.subarray(3) : bytes),
|
|
467
|
+
bom
|
|
468
|
+
};
|
|
469
|
+
} catch (unused) {
|
|
470
|
+
throw new EmbeddedWriteError('The properties in this file are not valid text.');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function encodePart(part) {
|
|
474
|
+
const body = new TextEncoder().encode(part.text);
|
|
475
|
+
if (!part.bom) return body;
|
|
476
|
+
const out = new Uint8Array(body.length + 3);
|
|
477
|
+
out.set([
|
|
478
|
+
0xef,
|
|
479
|
+
0xbb,
|
|
480
|
+
0xbf
|
|
481
|
+
]);
|
|
482
|
+
out.set(body, 3);
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
function readPartText(archive, entry) {
|
|
486
|
+
if (!entry) return null;
|
|
487
|
+
const bytes = readZipEntry(archive, entry, PART_MAX_BYTES);
|
|
488
|
+
return bytes ? decodeForRead(bytes) : null;
|
|
489
|
+
}
|
|
490
|
+
function packageLayout(archive) {
|
|
491
|
+
const relationships = findPart(archive, ROOT_RELATIONSHIPS_PART);
|
|
492
|
+
const parts = {};
|
|
493
|
+
const text = readPartText(archive, relationships);
|
|
494
|
+
const root = text === null ? null : parseXml(text);
|
|
495
|
+
if (!root || root.local !== 'Relationships' || root.ns !== NS_RELATIONSHIPS) {
|
|
496
|
+
return {
|
|
497
|
+
parts,
|
|
498
|
+
relationships
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
for (const child of root.children){
|
|
502
|
+
var _ref;
|
|
503
|
+
var _xmlAttribute, _xmlAttribute1, _xmlAttribute2;
|
|
504
|
+
if (child.local !== 'Relationship' || child.ns !== NS_RELATIONSHIPS) continue;
|
|
505
|
+
if (((_xmlAttribute = xmlAttribute(child, 'TargetMode')) == null ? void 0 : _xmlAttribute.value) === 'External') continue;
|
|
506
|
+
const role = roleOf((_ref = (_xmlAttribute1 = xmlAttribute(child, 'Type')) == null ? void 0 : _xmlAttribute1.value) != null ? _ref : '');
|
|
507
|
+
const target = (_xmlAttribute2 = xmlAttribute(child, 'Target')) == null ? void 0 : _xmlAttribute2.value;
|
|
508
|
+
if (role && target && !parts[role]) parts[role] = resolveTarget(target);
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
parts,
|
|
512
|
+
relationships
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/** Where each canonical key lives in the core properties part. */ const CORE_FIELDS = {
|
|
516
|
+
title: {
|
|
517
|
+
ns: NS_DC,
|
|
518
|
+
local: 'title',
|
|
519
|
+
prefix: 'dc'
|
|
520
|
+
},
|
|
521
|
+
subject: {
|
|
522
|
+
ns: NS_DC,
|
|
523
|
+
local: 'subject',
|
|
524
|
+
prefix: 'dc'
|
|
525
|
+
},
|
|
526
|
+
creator: {
|
|
527
|
+
ns: NS_DC,
|
|
528
|
+
local: 'creator',
|
|
529
|
+
prefix: 'dc'
|
|
530
|
+
},
|
|
531
|
+
keywords: {
|
|
532
|
+
ns: NS_CP,
|
|
533
|
+
local: 'keywords',
|
|
534
|
+
prefix: 'cp'
|
|
535
|
+
},
|
|
536
|
+
description: {
|
|
537
|
+
ns: NS_DC,
|
|
538
|
+
local: 'description',
|
|
539
|
+
prefix: 'dc'
|
|
540
|
+
},
|
|
541
|
+
category: {
|
|
542
|
+
ns: NS_CP,
|
|
543
|
+
local: 'category',
|
|
544
|
+
prefix: 'cp'
|
|
545
|
+
},
|
|
546
|
+
lastModifiedBy: {
|
|
547
|
+
ns: NS_CP,
|
|
548
|
+
local: 'lastModifiedBy',
|
|
549
|
+
prefix: 'cp'
|
|
550
|
+
},
|
|
551
|
+
createdAt: {
|
|
552
|
+
ns: NS_DCTERMS,
|
|
553
|
+
local: 'created',
|
|
554
|
+
prefix: 'dcterms'
|
|
555
|
+
},
|
|
556
|
+
modifiedAt: {
|
|
557
|
+
ns: NS_DCTERMS,
|
|
558
|
+
local: 'modified',
|
|
559
|
+
prefix: 'dcterms'
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
/** The core keys an edit may set. The dates and last-modified-by are facts. */ const CORE_WRITABLE = new Set([
|
|
563
|
+
'title',
|
|
564
|
+
'subject',
|
|
565
|
+
'creator',
|
|
566
|
+
'keywords',
|
|
567
|
+
'description',
|
|
568
|
+
'category'
|
|
569
|
+
]);
|
|
570
|
+
const isCoreKey = (key)=>Object.prototype.hasOwnProperty.call(CORE_FIELDS, key);
|
|
571
|
+
const splitList = (value, separator)=>value.split(separator).map((item)=>item.trim()).filter(Boolean);
|
|
572
|
+
/**
|
|
573
|
+
* A W3CDTF date (the profile of ISO 8601 `dcterms:W3CDTF` names) in the
|
|
574
|
+
* catalog's form: seconds kept, a fraction cut to milliseconds, the zone as
|
|
575
|
+
* `Z` or `±HH:MM` and only when one was written. Anything else is returned
|
|
576
|
+
* trimmed, as found.
|
|
577
|
+
*/ function normalizeW3cdtf(raw) {
|
|
578
|
+
const value = raw.trim();
|
|
579
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}(?::\d{2})?)(?:[.,](\d+))?(Z|[+-]\d{2}:\d{2})?$/.exec(value);
|
|
580
|
+
if (!match) return value;
|
|
581
|
+
const [, date, time, fraction, zone] = match;
|
|
582
|
+
return `${date}T${time}${fraction ? `.${fraction.slice(0, 3)}` : ''}${zone != null ? zone : ''}`;
|
|
583
|
+
}
|
|
584
|
+
function readCore(text) {
|
|
585
|
+
const root = parseXml(text);
|
|
586
|
+
if (!root || root.ns !== NS_CP || root.local !== 'coreProperties') return [];
|
|
587
|
+
const out = [];
|
|
588
|
+
const creators = [];
|
|
589
|
+
const keywords = [];
|
|
590
|
+
const seen = new Set();
|
|
591
|
+
for (const child of root.children){
|
|
592
|
+
const key = Object.keys(CORE_FIELDS).find((candidate)=>CORE_FIELDS[candidate].ns === child.ns && CORE_FIELDS[candidate].local === child.local);
|
|
593
|
+
if (!key) continue;
|
|
594
|
+
const value = xmlTextContent(child).trim();
|
|
595
|
+
if (!value) continue;
|
|
596
|
+
// Office writes several authors into one `dc:creator`, joined by `;`.
|
|
597
|
+
if (key === 'creator') creators.push(...splitList(value, /;/));
|
|
598
|
+
else if (key === 'keywords') keywords.push(...splitList(value, /[,;]/));
|
|
599
|
+
else if (!seen.has(key)) {
|
|
600
|
+
seen.add(key);
|
|
601
|
+
out.push({
|
|
602
|
+
key,
|
|
603
|
+
value: key === 'createdAt' || key === 'modifiedAt' ? normalizeW3cdtf(value) : value,
|
|
604
|
+
source: 'ooxml'
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (creators.length) out.push({
|
|
609
|
+
key: 'creator',
|
|
610
|
+
value: creators,
|
|
611
|
+
source: 'ooxml'
|
|
612
|
+
});
|
|
613
|
+
if (keywords.length) out.push({
|
|
614
|
+
key: 'keywords',
|
|
615
|
+
value: keywords,
|
|
616
|
+
source: 'ooxml'
|
|
617
|
+
});
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
function readExtended(text) {
|
|
621
|
+
const root = parseXml(text);
|
|
622
|
+
if (!root || !root.ns || !NS_EXTENDED.has(root.ns) || root.local !== 'Properties') return [];
|
|
623
|
+
const value = (local)=>{
|
|
624
|
+
const child = root.children.find((element)=>element.local === local && element.ns === root.ns);
|
|
625
|
+
return child ? xmlTextContent(child).trim() : '';
|
|
626
|
+
};
|
|
627
|
+
const out = [];
|
|
628
|
+
const application = value('Application');
|
|
629
|
+
if (application) out.push({
|
|
630
|
+
key: 'software',
|
|
631
|
+
value: application,
|
|
632
|
+
source: 'ooxml'
|
|
633
|
+
});
|
|
634
|
+
const company = value('Company');
|
|
635
|
+
if (company) out.push({
|
|
636
|
+
key: 'company',
|
|
637
|
+
value: company,
|
|
638
|
+
source: 'ooxml'
|
|
639
|
+
});
|
|
640
|
+
// Word counts pages and PowerPoint slides; a workbook has neither.
|
|
641
|
+
const count = [
|
|
642
|
+
value('Pages'),
|
|
643
|
+
value('Slides')
|
|
644
|
+
].find((item)=>/^\d+$/.test(item));
|
|
645
|
+
if (count) out.push({
|
|
646
|
+
key: 'pageCount',
|
|
647
|
+
value: count,
|
|
648
|
+
source: 'ooxml'
|
|
649
|
+
});
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
/** Text-valued variant types — the only ones an edit can write (§22.4). */ const VT_TEXT = new Set([
|
|
653
|
+
'lpwstr',
|
|
654
|
+
'lpstr'
|
|
655
|
+
]);
|
|
656
|
+
const VT_READ_ONLY_TEXT = new Set([
|
|
657
|
+
'bstr'
|
|
658
|
+
]);
|
|
659
|
+
const VT_NUMBER = new Set([
|
|
660
|
+
'i1',
|
|
661
|
+
'i2',
|
|
662
|
+
'i4',
|
|
663
|
+
'i8',
|
|
664
|
+
'int',
|
|
665
|
+
'ui1',
|
|
666
|
+
'ui2',
|
|
667
|
+
'ui4',
|
|
668
|
+
'ui8',
|
|
669
|
+
'uint',
|
|
670
|
+
'r4',
|
|
671
|
+
'r8',
|
|
672
|
+
'decimal'
|
|
673
|
+
]);
|
|
674
|
+
const VT_DATE = new Set([
|
|
675
|
+
'filetime',
|
|
676
|
+
'date'
|
|
677
|
+
]);
|
|
678
|
+
/** A custom property's name, from its `name` attribute. */ const propertyName = (property)=>{
|
|
679
|
+
var _ref;
|
|
680
|
+
var _xmlAttribute;
|
|
681
|
+
return (_ref = (_xmlAttribute = xmlAttribute(property, 'name')) == null ? void 0 : _xmlAttribute.value) != null ? _ref : '';
|
|
682
|
+
};
|
|
683
|
+
/** The one `vt:` value child of a custom `property` (§22.3.2.2). */ const propertyValue = (property)=>property.children.find((child)=>child.ns !== null && NS_VT.has(child.ns));
|
|
684
|
+
function customProperties(root) {
|
|
685
|
+
return root.children.filter((child)=>child.local === 'property' && child.ns === root.ns);
|
|
686
|
+
}
|
|
687
|
+
function readCustom(text) {
|
|
688
|
+
const root = parseXml(text);
|
|
689
|
+
if (!root || !root.ns || !NS_CUSTOM.has(root.ns) || root.local !== 'Properties') return [];
|
|
690
|
+
const out = [];
|
|
691
|
+
const seen = new Set();
|
|
692
|
+
for (const property of customProperties(root)){
|
|
693
|
+
const name = propertyName(property);
|
|
694
|
+
const valueElement = propertyValue(property);
|
|
695
|
+
if (!name || !valueElement || seen.has(name)) continue;
|
|
696
|
+
const type = valueElement.local;
|
|
697
|
+
const raw = xmlTextContent(valueElement);
|
|
698
|
+
let value;
|
|
699
|
+
if (VT_TEXT.has(type) || VT_READ_ONLY_TEXT.has(type)) value = raw;
|
|
700
|
+
else if (VT_NUMBER.has(type)) value = raw.trim();
|
|
701
|
+
else if (type === 'bool') {
|
|
702
|
+
const flag = raw.trim().toLowerCase();
|
|
703
|
+
value = flag === '1' ? 'true' : flag === '0' ? 'false' : flag;
|
|
704
|
+
} else if (VT_DATE.has(type)) value = normalizeW3cdtf(raw);
|
|
705
|
+
else continue;
|
|
706
|
+
if (!value.trim()) continue;
|
|
707
|
+
seen.add(name);
|
|
708
|
+
out.push(_extends({
|
|
709
|
+
key: otherEmbeddedKey('ooxml', name),
|
|
710
|
+
value,
|
|
711
|
+
source: 'ooxml',
|
|
712
|
+
label: name
|
|
713
|
+
}, VT_TEXT.has(type) ? {} : {
|
|
714
|
+
editable: false
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Read an Office file's document properties.
|
|
721
|
+
*
|
|
722
|
+
* Null when the bytes are not an OPC package — not a ZIP, or a ZIP with no
|
|
723
|
+
* `[Content_Types].xml`. A package whose property parts are missing or
|
|
724
|
+
* unreadable reads as an empty list: a damaged part costs its own fields,
|
|
725
|
+
* not the others'. A part with a DOCTYPE is unreadable by design.
|
|
726
|
+
*/ export function readOoxml(bytes) {
|
|
727
|
+
const archive = readZip(bytes);
|
|
728
|
+
if (!archive || !findPart(archive, CONTENT_TYPES_PART)) return null;
|
|
729
|
+
const { parts } = packageLayout(archive);
|
|
730
|
+
const candidates = [];
|
|
731
|
+
const part = (role)=>{
|
|
732
|
+
const name = parts[role];
|
|
733
|
+
return name === undefined ? null : readPartText(archive, findPart(archive, name));
|
|
734
|
+
};
|
|
735
|
+
const core = part('core');
|
|
736
|
+
if (core !== null) candidates.push(...readCore(core));
|
|
737
|
+
const extended = part('extended');
|
|
738
|
+
if (extended !== null) candidates.push(...readExtended(extended));
|
|
739
|
+
const custom = part('custom');
|
|
740
|
+
if (custom !== null) candidates.push(...readCustom(custom));
|
|
741
|
+
return {
|
|
742
|
+
candidates
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
// Writing
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
/** Office's own serialization of `dcterms:modified`: seconds, UTC. */ const w3cdtfNow = (now)=>now.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
749
|
+
function patchText(key, value) {
|
|
750
|
+
if (!Array.isArray(value)) return value.trim();
|
|
751
|
+
const items = value.map((item)=>item.trim()).filter(Boolean);
|
|
752
|
+
// Office joins authors with ";" and keywords with ","; both read back.
|
|
753
|
+
return items.join(key === 'creator' ? '; ' : ', ');
|
|
754
|
+
}
|
|
755
|
+
function parsePart(text) {
|
|
756
|
+
const root = parseXml(text);
|
|
757
|
+
if (!root) {
|
|
758
|
+
throw new EmbeddedWriteError('The document properties in this file are damaged, so they cannot be edited.');
|
|
759
|
+
}
|
|
760
|
+
return root;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* The qualified name, and any declaration it needs, for a new element or
|
|
764
|
+
* attribute. An attribute cannot use the default namespace — unprefixed, it
|
|
765
|
+
* would be in none — so it always gets a prefix.
|
|
766
|
+
*/ function qualify(scope, ns, local, conventional, attribute = false) {
|
|
767
|
+
const prefix = prefixFor(scope, ns);
|
|
768
|
+
if (prefix !== undefined && !(attribute && prefix === '')) {
|
|
769
|
+
return {
|
|
770
|
+
name: prefix ? `${prefix}:${local}` : local,
|
|
771
|
+
declare: ''
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
name: `${conventional}:${local}`,
|
|
776
|
+
declare: ` xmlns:${conventional}="${escapeXmlAttribute(ns)}"`
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
/** Replace an element's content, expanding it first if it was self-closing. */ function replaceContent(source, element, escaped) {
|
|
780
|
+
if (!element.selfClosing) {
|
|
781
|
+
return {
|
|
782
|
+
start: element.openEnd,
|
|
783
|
+
end: element.closeStart,
|
|
784
|
+
text: escaped
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const openTag = source.slice(element.start, element.openEnd).replace(/\s*\/>$/, '>');
|
|
788
|
+
return {
|
|
789
|
+
start: element.start,
|
|
790
|
+
end: element.end,
|
|
791
|
+
text: `${openTag}${escaped}</${element.name}>`
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/** Insert markup as the last content of `root`. */ function appendTo(source, root, markup) {
|
|
795
|
+
if (!root.selfClosing) {
|
|
796
|
+
return {
|
|
797
|
+
start: root.closeStart,
|
|
798
|
+
end: root.closeStart,
|
|
799
|
+
text: markup
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const openTag = source.slice(root.start, root.openEnd).replace(/\s*\/>$/, '>');
|
|
803
|
+
return {
|
|
804
|
+
start: root.start,
|
|
805
|
+
end: root.end,
|
|
806
|
+
text: `${openTag}${markup}</${root.name}>`
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function editCore(source, values, now) {
|
|
810
|
+
const root = parsePart(source);
|
|
811
|
+
if (root.ns !== NS_CP || root.local !== 'coreProperties') {
|
|
812
|
+
throw new EmbeddedWriteError('The document properties in this file are damaged, so they cannot be edited.');
|
|
813
|
+
}
|
|
814
|
+
const edits = [];
|
|
815
|
+
const appended = [];
|
|
816
|
+
const all = new Map(values);
|
|
817
|
+
all.set('modifiedAt', w3cdtfNow(now));
|
|
818
|
+
for (const [key, value] of all){
|
|
819
|
+
const field = CORE_FIELDS[key];
|
|
820
|
+
const found = root.children.filter((child)=>child.ns === field.ns && child.local === field.local);
|
|
821
|
+
const [first, ...rest] = found;
|
|
822
|
+
if (value === null) {
|
|
823
|
+
for (const element of found){
|
|
824
|
+
edits.push({
|
|
825
|
+
start: element.start,
|
|
826
|
+
end: element.end,
|
|
827
|
+
text: ''
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const escaped = escapeXmlText(value);
|
|
833
|
+
if (first) {
|
|
834
|
+
edits.push(replaceContent(source, first, escaped));
|
|
835
|
+
// One value per property: a duplicate would be read instead of it.
|
|
836
|
+
for (const element of rest){
|
|
837
|
+
edits.push({
|
|
838
|
+
start: element.start,
|
|
839
|
+
end: element.end,
|
|
840
|
+
text: ''
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
const { name, declare } = qualify(root, field.ns, field.local, field.prefix);
|
|
846
|
+
let attributes = declare;
|
|
847
|
+
if (key === 'modifiedAt') {
|
|
848
|
+
// `xsi:type` names W3CDTF by QName, so its prefix must be in scope.
|
|
849
|
+
const xsi = qualify(root, NS_XSI, 'type', 'xsi', true);
|
|
850
|
+
const dctermsPrefix = name.includes(':') ? name.slice(0, name.indexOf(':')) : '';
|
|
851
|
+
attributes += `${xsi.declare} ${xsi.name}="${dctermsPrefix ? `${dctermsPrefix}:` : ''}W3CDTF"`;
|
|
852
|
+
}
|
|
853
|
+
appended.push(`<${name}${attributes}>${escaped}</${name}>`);
|
|
854
|
+
}
|
|
855
|
+
if (appended.length) edits.push(appendTo(source, root, appended.join('')));
|
|
856
|
+
return applyEdits(source, edits);
|
|
857
|
+
}
|
|
858
|
+
function editCustom(source, values) {
|
|
859
|
+
const root = parsePart(source);
|
|
860
|
+
if (!root.ns || !NS_CUSTOM.has(root.ns) || root.local !== 'Properties') {
|
|
861
|
+
throw new EmbeddedWriteError('The custom properties in this file are damaged, so they cannot be edited.');
|
|
862
|
+
}
|
|
863
|
+
const properties = customProperties(root);
|
|
864
|
+
const edits = [];
|
|
865
|
+
const removed = new Set();
|
|
866
|
+
for (const [name, value] of values){
|
|
867
|
+
const property = properties.find((element)=>propertyName(element) === name);
|
|
868
|
+
if (!property) {
|
|
869
|
+
throw new EmbeddedWriteError(`"${name}" is not a custom property of this file.`);
|
|
870
|
+
}
|
|
871
|
+
if (value === null) {
|
|
872
|
+
edits.push({
|
|
873
|
+
start: property.start,
|
|
874
|
+
end: property.end,
|
|
875
|
+
text: ''
|
|
876
|
+
});
|
|
877
|
+
removed.add(property);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const valueElement = propertyValue(property);
|
|
881
|
+
if (!valueElement || !VT_TEXT.has(valueElement.local)) {
|
|
882
|
+
throw new EmbeddedWriteError(`"${name}" is not a text property, so it cannot be edited here.`);
|
|
883
|
+
}
|
|
884
|
+
edits.push(replaceContent(source, valueElement, escapeXmlText(value)));
|
|
885
|
+
}
|
|
886
|
+
// A `pid` must be an integer of at least 2, unique in the part
|
|
887
|
+
// (§22.3.2.2). Removing a property cannot break that, so only a pid that
|
|
888
|
+
// was already broken is renumbered; a valid one never moves.
|
|
889
|
+
const remaining = properties.filter((property)=>!removed.has(property));
|
|
890
|
+
const pidOf = (property)=>{
|
|
891
|
+
var _ref;
|
|
892
|
+
var _xmlAttribute;
|
|
893
|
+
const raw = (_ref = (_xmlAttribute = xmlAttribute(property, 'pid')) == null ? void 0 : _xmlAttribute.value.trim()) != null ? _ref : '';
|
|
894
|
+
return /^\d+$/.test(raw) ? Number(raw) : NaN;
|
|
895
|
+
};
|
|
896
|
+
let next = Math.max(1, ...remaining.map(pidOf).filter((pid)=>pid >= 2));
|
|
897
|
+
const used = new Set();
|
|
898
|
+
for (const property of remaining){
|
|
899
|
+
const pid = pidOf(property);
|
|
900
|
+
if (pid >= 2 && !used.has(pid)) {
|
|
901
|
+
used.add(pid);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
next += 1;
|
|
905
|
+
const attribute = xmlAttribute(property, 'pid');
|
|
906
|
+
if (attribute) {
|
|
907
|
+
edits.push({
|
|
908
|
+
start: attribute.valueStart,
|
|
909
|
+
end: attribute.valueEnd,
|
|
910
|
+
text: String(next)
|
|
911
|
+
});
|
|
912
|
+
} else {
|
|
913
|
+
const at = property.start + 1 + property.name.length;
|
|
914
|
+
edits.push({
|
|
915
|
+
start: at,
|
|
916
|
+
end: at,
|
|
917
|
+
text: ` pid="${next}"`
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return applyEdits(source, edits);
|
|
922
|
+
}
|
|
923
|
+
/** Add the core part's content-type override, if it has none (Part 2 §10.1.2). */ function editContentTypes(source, partName) {
|
|
924
|
+
const root = parsePart(source);
|
|
925
|
+
if (root.ns !== NS_CONTENT_TYPES || root.local !== 'Types') {
|
|
926
|
+
throw new EmbeddedWriteError('The package index of this file is damaged.');
|
|
927
|
+
}
|
|
928
|
+
const wanted = `/${partName}`.toLowerCase();
|
|
929
|
+
const has = root.children.some((child)=>{
|
|
930
|
+
var _xmlAttribute;
|
|
931
|
+
return child.local === 'Override' && ((_xmlAttribute = xmlAttribute(child, 'PartName')) == null ? void 0 : _xmlAttribute.value.toLowerCase()) === wanted;
|
|
932
|
+
});
|
|
933
|
+
if (has) return source;
|
|
934
|
+
const name = root.prefix ? `${root.prefix}:Override` : 'Override';
|
|
935
|
+
return applyEdits(source, [
|
|
936
|
+
appendTo(source, root, `<${name} PartName="/${escapeXmlAttribute(partName)}" ContentType="${CORE_CONTENT_TYPE}"/>`)
|
|
937
|
+
]);
|
|
938
|
+
}
|
|
939
|
+
/** Add the package relationship to a new core part (Part 2 §9.3). */ function editRelationships(source, partName) {
|
|
940
|
+
const root = parsePart(source);
|
|
941
|
+
if (root.ns !== NS_RELATIONSHIPS || root.local !== 'Relationships') {
|
|
942
|
+
throw new EmbeddedWriteError('The package relationships of this file are damaged.');
|
|
943
|
+
}
|
|
944
|
+
const ids = new Set(root.children.map((child)=>{
|
|
945
|
+
var _xmlAttribute;
|
|
946
|
+
return (_xmlAttribute = xmlAttribute(child, 'Id')) == null ? void 0 : _xmlAttribute.value;
|
|
947
|
+
}));
|
|
948
|
+
let index = root.children.length + 1;
|
|
949
|
+
while(ids.has(`rId${index}`))index++;
|
|
950
|
+
const name = root.prefix ? `${root.prefix}:Relationship` : 'Relationship';
|
|
951
|
+
return applyEdits(source, [
|
|
952
|
+
appendTo(source, root, `<${name} Id="rId${index}" Type="${CORE_RELATIONSHIP}" Target="${escapeXmlAttribute(partName)}"/>`)
|
|
953
|
+
]);
|
|
954
|
+
}
|
|
955
|
+
/** An empty core properties part, declared the way Office declares it. */ const EMPTY_CORE = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n' + `<cp:coreProperties xmlns:cp="${NS_CP}" xmlns:dc="${NS_DC}" xmlns:dcterms="${NS_DCTERMS}" xmlns:dcmitype="${NS_DCMITYPE}" xmlns:xsi="${NS_XSI}"></cp:coreProperties>`;
|
|
956
|
+
function readEditable(archive, entry) {
|
|
957
|
+
if (entry.encrypted) {
|
|
958
|
+
throw new EmbeddedWriteError('This file is encrypted, so its details can only be read.');
|
|
959
|
+
}
|
|
960
|
+
const bytes = readZipEntry(archive, entry, PART_MAX_BYTES);
|
|
961
|
+
if (!bytes) {
|
|
962
|
+
throw new EmbeddedWriteError('The document properties in this file are damaged, so they cannot be edited.');
|
|
963
|
+
}
|
|
964
|
+
return decodeForEdit(bytes);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Write an edit into an Office file's properties.
|
|
968
|
+
*
|
|
969
|
+
* Takes the canonical keys `title`, `subject`, `creator` (joined `"; "`),
|
|
970
|
+
* `keywords` (joined `", "`), `description` and `category`, and
|
|
971
|
+
* `ooxml|<name>` for a custom text property the file already holds; `null`
|
|
972
|
+
* removes. `dcterms:modified` is always set to `now`. A file with no core
|
|
973
|
+
* part gets one — plus its content-type override and package relationship.
|
|
974
|
+
*
|
|
975
|
+
* Throws {@link EmbeddedWriteError} for any other key, a custom property the
|
|
976
|
+
* file lacks or that is not text, and a package it cannot edit safely.
|
|
977
|
+
*/ export function writeOoxml(bytes, patch, options) {
|
|
978
|
+
const archive = readZip(bytes);
|
|
979
|
+
if (!archive || !findPart(archive, CONTENT_TYPES_PART)) {
|
|
980
|
+
throw new EmbeddedWriteError('This file is not an Office document.');
|
|
981
|
+
}
|
|
982
|
+
const core = new Map();
|
|
983
|
+
const custom = new Map();
|
|
984
|
+
for (const [key, raw] of Object.entries(patch)){
|
|
985
|
+
const value = raw === null ? null : patchText(key, raw) || null;
|
|
986
|
+
if (isCoreKey(key) && CORE_WRITABLE.has(key)) {
|
|
987
|
+
core.set(key, value);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const other = parseOtherEmbeddedKey(key);
|
|
991
|
+
if ((other == null ? void 0 : other.namespace) === 'ooxml') {
|
|
992
|
+
custom.set(other.parts.join('|'), value);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
throw new EmbeddedWriteError(`${embeddedFieldLabel(key, 'ooxml')} cannot be written into an Office document.`);
|
|
996
|
+
}
|
|
997
|
+
const layout = packageLayout(archive);
|
|
998
|
+
const changes = new Map();
|
|
999
|
+
const verify = (name, part)=>{
|
|
1000
|
+
if (!parseXml(part.text)) {
|
|
1001
|
+
throw new EmbeddedWriteError('The edited properties did not verify, so nothing was saved.');
|
|
1002
|
+
}
|
|
1003
|
+
changes.set(name, encodePart(part));
|
|
1004
|
+
};
|
|
1005
|
+
if (custom.size) {
|
|
1006
|
+
const name = layout.parts.custom;
|
|
1007
|
+
const entry = name === undefined ? undefined : findPart(archive, name);
|
|
1008
|
+
if (!entry) {
|
|
1009
|
+
const [first] = custom.keys();
|
|
1010
|
+
throw new EmbeddedWriteError(`"${first}" is not a custom property of this file.`);
|
|
1011
|
+
}
|
|
1012
|
+
const part = readEditable(archive, entry);
|
|
1013
|
+
verify(entry.name, _extends({}, part, {
|
|
1014
|
+
text: editCustom(part.text, custom)
|
|
1015
|
+
}));
|
|
1016
|
+
}
|
|
1017
|
+
const coreName = layout.parts.core;
|
|
1018
|
+
const coreEntry = coreName === undefined ? undefined : findPart(archive, coreName);
|
|
1019
|
+
if (coreEntry) {
|
|
1020
|
+
const part = readEditable(archive, coreEntry);
|
|
1021
|
+
verify(coreEntry.name, _extends({}, part, {
|
|
1022
|
+
text: editCore(part.text, core, options.now)
|
|
1023
|
+
}));
|
|
1024
|
+
} else {
|
|
1025
|
+
// No core part: create one, declare its type, and relate it to the
|
|
1026
|
+
// package — or, when a relationship already names a missing part,
|
|
1027
|
+
// create the part it names.
|
|
1028
|
+
let name = coreName;
|
|
1029
|
+
if (name === undefined) {
|
|
1030
|
+
name = DEFAULT_CORE_PART;
|
|
1031
|
+
for(let index = 1; findPart(archive, name); index++){
|
|
1032
|
+
name = `docProps/core${index}.xml`;
|
|
1033
|
+
}
|
|
1034
|
+
if (!layout.relationships) {
|
|
1035
|
+
throw new EmbeddedWriteError('This Office file has no package relationships.');
|
|
1036
|
+
}
|
|
1037
|
+
const rels = readEditable(archive, layout.relationships);
|
|
1038
|
+
verify(layout.relationships.name, _extends({}, rels, {
|
|
1039
|
+
text: editRelationships(rels.text, name)
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1042
|
+
const types = findPart(archive, CONTENT_TYPES_PART);
|
|
1043
|
+
if (!types) throw new EmbeddedWriteError('This file is not an Office document.');
|
|
1044
|
+
const typesPart = readEditable(archive, types);
|
|
1045
|
+
verify(types.name, _extends({}, typesPart, {
|
|
1046
|
+
text: editContentTypes(typesPart.text, name)
|
|
1047
|
+
}));
|
|
1048
|
+
verify(name, {
|
|
1049
|
+
text: editCore(EMPTY_CORE, core, options.now),
|
|
1050
|
+
bom: false
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
return rewriteZip(archive, changes, {
|
|
1054
|
+
now: options.now
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
//# sourceMappingURL=ooxml.js.map
|