odf.js 0.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +729 -0
- package/dist/index.d.cts +389 -0
- package/dist/index.d.ts +389 -0
- package/dist/index.js +683 -0
- package/package.json +86 -2
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let zod = require("zod");
|
|
3
|
+
let fast_xml_parser = require("fast-xml-parser");
|
|
4
|
+
let fflate = require("fflate");
|
|
5
|
+
//#region src/model/node.ts
|
|
6
|
+
const AttributeSchema = zod.z.object({
|
|
7
|
+
name: zod.z.string(),
|
|
8
|
+
value: zod.z.string()
|
|
9
|
+
});
|
|
10
|
+
const XmlTextSchema = zod.z.object({
|
|
11
|
+
type: zod.z.literal("text"),
|
|
12
|
+
value: zod.z.string()
|
|
13
|
+
});
|
|
14
|
+
const XmlCdataSchema = zod.z.object({
|
|
15
|
+
type: zod.z.literal("cdata"),
|
|
16
|
+
value: zod.z.string()
|
|
17
|
+
});
|
|
18
|
+
const XmlCommentSchema = zod.z.object({
|
|
19
|
+
type: zod.z.literal("comment"),
|
|
20
|
+
value: zod.z.string()
|
|
21
|
+
});
|
|
22
|
+
const XmlDeclarationSchema = zod.z.object({
|
|
23
|
+
type: zod.z.literal("declaration"),
|
|
24
|
+
attributes: zod.z.array(AttributeSchema)
|
|
25
|
+
});
|
|
26
|
+
const XmlPiSchema = zod.z.object({
|
|
27
|
+
type: zod.z.literal("pi"),
|
|
28
|
+
target: zod.z.string(),
|
|
29
|
+
content: zod.z.string()
|
|
30
|
+
});
|
|
31
|
+
function isRecord$1(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function isAttribute(value) {
|
|
35
|
+
return isRecord$1(value) && typeof value.name === "string" && typeof value.value === "string";
|
|
36
|
+
}
|
|
37
|
+
function isXmlNode(value) {
|
|
38
|
+
if (!isRecord$1(value)) return false;
|
|
39
|
+
const t = value.type;
|
|
40
|
+
if (t === "text" || t === "cdata" || t === "comment") return typeof value.value === "string";
|
|
41
|
+
if (t === "declaration") return Array.isArray(value.attributes) && value.attributes.every(isAttribute);
|
|
42
|
+
if (t === "pi") return typeof value.target === "string" && typeof value.content === "string";
|
|
43
|
+
if (t === "element") return typeof value.tag === "string" && Array.isArray(value.attributes) && value.attributes.every(isAttribute) && Array.isArray(value.children) && value.children.every(isXmlNode);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const XmlElementSchema = zod.z.object({
|
|
47
|
+
type: zod.z.literal("element"),
|
|
48
|
+
tag: zod.z.string(),
|
|
49
|
+
attributes: zod.z.array(AttributeSchema),
|
|
50
|
+
children: zod.z.array(zod.z.custom(isXmlNode))
|
|
51
|
+
});
|
|
52
|
+
const XmlNodeSchema = zod.z.discriminatedUnion("type", [
|
|
53
|
+
XmlTextSchema,
|
|
54
|
+
XmlCdataSchema,
|
|
55
|
+
XmlCommentSchema,
|
|
56
|
+
XmlDeclarationSchema,
|
|
57
|
+
XmlPiSchema,
|
|
58
|
+
XmlElementSchema
|
|
59
|
+
]);
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/model/package.ts
|
|
62
|
+
const XmlPartSchema = zod.z.object({
|
|
63
|
+
kind: zod.z.literal("xml"),
|
|
64
|
+
nodes: zod.z.array(XmlNodeSchema)
|
|
65
|
+
});
|
|
66
|
+
const BinaryPartSchema = zod.z.object({
|
|
67
|
+
kind: zod.z.literal("binary"),
|
|
68
|
+
base64: zod.z.string()
|
|
69
|
+
});
|
|
70
|
+
const PartSchema = zod.z.discriminatedUnion("kind", [XmlPartSchema, BinaryPartSchema]);
|
|
71
|
+
const PackageSchema = zod.z.object({ parts: zod.z.record(zod.z.string(), PartSchema) });
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/util/base64.ts
|
|
74
|
+
const TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
75
|
+
const DECODE = (() => {
|
|
76
|
+
const map = (/* @__PURE__ */ new Uint8Array(256)).fill(255);
|
|
77
|
+
for (let i = 0; i < 64; i = i + 1) map[TABLE.charCodeAt(i)] = i;
|
|
78
|
+
return map;
|
|
79
|
+
})();
|
|
80
|
+
function bytesToBase64(bytes) {
|
|
81
|
+
let out = "";
|
|
82
|
+
const len = bytes.length;
|
|
83
|
+
for (let i = 0; i < len; i = i + 3) {
|
|
84
|
+
const b0 = bytes[i];
|
|
85
|
+
const b1 = i + 1 < len ? bytes[i + 1] : 0;
|
|
86
|
+
const b2 = i + 2 < len ? bytes[i + 2] : 0;
|
|
87
|
+
out += TABLE[b0 >> 2];
|
|
88
|
+
out += TABLE[(b0 & 3) << 4 | b1 >> 4];
|
|
89
|
+
out += i + 1 < len ? TABLE[(b1 & 15) << 2 | b2 >> 6] : "=";
|
|
90
|
+
out += i + 2 < len ? TABLE[b2 & 63] : "=";
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
function base64ToBytes(b64) {
|
|
95
|
+
const clean = b64.replace(/[^A-Za-z0-9+/=]/g, "");
|
|
96
|
+
const len = clean.length;
|
|
97
|
+
const out = new Uint8Array(len * 3 / 4 | 0);
|
|
98
|
+
let p = 0;
|
|
99
|
+
for (let i = 0; i < len; i = i + 4) {
|
|
100
|
+
const c0 = DECODE[clean.charCodeAt(i)];
|
|
101
|
+
const c1 = DECODE[clean.charCodeAt(i + 1)];
|
|
102
|
+
const c2 = clean.charCodeAt(i + 2);
|
|
103
|
+
const c3 = clean.charCodeAt(i + 3);
|
|
104
|
+
if (c0 === 255 || c1 === 255) throw new Error("invalid base64 input");
|
|
105
|
+
out[p++] = c0 << 2 | c1 >> 4;
|
|
106
|
+
if (c2 !== 61) {
|
|
107
|
+
const d2 = DECODE[c2];
|
|
108
|
+
out[p++] = (c1 & 15) << 4 | d2 >> 2;
|
|
109
|
+
if (c3 !== 61) {
|
|
110
|
+
const d3 = DECODE[c3];
|
|
111
|
+
out[p++] = (d2 & 3) << 6 | d3;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out.subarray(0, p);
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/xml/parse.ts
|
|
119
|
+
const PARSER = new fast_xml_parser.XMLParser({
|
|
120
|
+
preserveOrder: true,
|
|
121
|
+
attributeNamePrefix: "@_",
|
|
122
|
+
ignoreAttributes: false,
|
|
123
|
+
textNodeName: "#text",
|
|
124
|
+
cdataPropName: "__cdata",
|
|
125
|
+
commentPropName: "__comment",
|
|
126
|
+
processEntities: false,
|
|
127
|
+
parseTagValue: false,
|
|
128
|
+
trimValues: false
|
|
129
|
+
});
|
|
130
|
+
function parseXml(xml) {
|
|
131
|
+
return parseNodes(PARSER.parse(xml));
|
|
132
|
+
}
|
|
133
|
+
function isRecord(value) {
|
|
134
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
135
|
+
}
|
|
136
|
+
function isUnknownArray(value) {
|
|
137
|
+
return Array.isArray(value);
|
|
138
|
+
}
|
|
139
|
+
function asString(value) {
|
|
140
|
+
if (typeof value !== "string") throw new Error(`expected string while parsing XML, got ${typeof value}`);
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
function parseNodes(raw) {
|
|
144
|
+
if (!isUnknownArray(raw)) throw new Error("fast-xml-parser output was not an ordered array");
|
|
145
|
+
return raw.map(parseNode);
|
|
146
|
+
}
|
|
147
|
+
function parseNode(raw) {
|
|
148
|
+
if (!isRecord(raw)) throw new Error("fast-xml-parser node was not an object");
|
|
149
|
+
let tagKey;
|
|
150
|
+
for (const key of Object.keys(raw)) if (key !== ":@") {
|
|
151
|
+
if (tagKey !== void 0) throw new Error("XML node had multiple tag keys");
|
|
152
|
+
tagKey = key;
|
|
153
|
+
}
|
|
154
|
+
if (tagKey === void 0) throw new Error("XML node had no tag key");
|
|
155
|
+
const attributes = parseAttributes(raw[":@"]);
|
|
156
|
+
if (tagKey === "#text") return {
|
|
157
|
+
type: "text",
|
|
158
|
+
value: asString(raw["#text"])
|
|
159
|
+
};
|
|
160
|
+
if (tagKey === "__comment") return {
|
|
161
|
+
type: "comment",
|
|
162
|
+
value: scalarText(raw.__comment)
|
|
163
|
+
};
|
|
164
|
+
if (tagKey === "__cdata") return {
|
|
165
|
+
type: "cdata",
|
|
166
|
+
value: scalarText(raw.__cdata)
|
|
167
|
+
};
|
|
168
|
+
if (tagKey === "?xml") return {
|
|
169
|
+
type: "declaration",
|
|
170
|
+
attributes
|
|
171
|
+
};
|
|
172
|
+
if (tagKey.startsWith("?")) return {
|
|
173
|
+
type: "pi",
|
|
174
|
+
target: tagKey.slice(1),
|
|
175
|
+
content: scalarText(raw[tagKey])
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
type: "element",
|
|
179
|
+
tag: tagKey,
|
|
180
|
+
attributes,
|
|
181
|
+
children: parseNodes(raw[tagKey])
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function parseAttributes(raw) {
|
|
185
|
+
if (raw === void 0) return [];
|
|
186
|
+
if (!isRecord(raw)) throw new Error("XML attributes were not an object");
|
|
187
|
+
const attrs = [];
|
|
188
|
+
for (const key of Object.keys(raw)) {
|
|
189
|
+
if (!key.startsWith("@_")) throw new Error(`unexpected attribute key without @_ prefix: ${key}`);
|
|
190
|
+
attrs.push({
|
|
191
|
+
name: key.slice(2),
|
|
192
|
+
value: asString(raw[key])
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return attrs;
|
|
196
|
+
}
|
|
197
|
+
function scalarText(raw) {
|
|
198
|
+
if (!isUnknownArray(raw) || raw.length === 0) throw new Error("expected a scalar-text wrapper array");
|
|
199
|
+
const first = raw[0];
|
|
200
|
+
if (!isRecord(first)) throw new Error("scalar-text wrapper was not an object");
|
|
201
|
+
return asString(first["#text"]);
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/zip.ts
|
|
205
|
+
function unzipPackage(bytes) {
|
|
206
|
+
return (0, fflate.unzipSync)(bytes);
|
|
207
|
+
}
|
|
208
|
+
function zipPackage(entries) {
|
|
209
|
+
const data = {};
|
|
210
|
+
for (const [path, entry] of entries) if (entry.stored === true) data[path] = [entry.bytes, { level: 0 }];
|
|
211
|
+
else data[path] = entry.bytes;
|
|
212
|
+
return (0, fflate.zipSync)(data);
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/package-io/read.ts
|
|
216
|
+
function parsePackage(bytes) {
|
|
217
|
+
const entries = unzipPackage(bytes);
|
|
218
|
+
const parts = {};
|
|
219
|
+
for (const [path, partBytes] of Object.entries(entries)) if (looksLikeXml(partBytes)) parts[path] = {
|
|
220
|
+
kind: "xml",
|
|
221
|
+
nodes: parseXml(new TextDecoder("utf-8").decode(partBytes))
|
|
222
|
+
};
|
|
223
|
+
else parts[path] = {
|
|
224
|
+
kind: "binary",
|
|
225
|
+
base64: bytesToBase64(partBytes)
|
|
226
|
+
};
|
|
227
|
+
return { parts };
|
|
228
|
+
}
|
|
229
|
+
function looksLikeXml(bytes) {
|
|
230
|
+
let i = 0;
|
|
231
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) i = 3;
|
|
232
|
+
while (i < bytes.length) {
|
|
233
|
+
const b = bytes[i];
|
|
234
|
+
if (b === 32 || b === 9 || b === 10 || b === 13) {
|
|
235
|
+
i = i + 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
return b === 60;
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/xml/build.ts
|
|
244
|
+
const BUILDER = new fast_xml_parser.XMLBuilder({
|
|
245
|
+
preserveOrder: true,
|
|
246
|
+
attributeNamePrefix: "@_",
|
|
247
|
+
ignoreAttributes: false,
|
|
248
|
+
textNodeName: "#text",
|
|
249
|
+
cdataPropName: "__cdata",
|
|
250
|
+
commentPropName: "__comment",
|
|
251
|
+
processEntities: false,
|
|
252
|
+
format: false,
|
|
253
|
+
suppressEmptyNode: false
|
|
254
|
+
});
|
|
255
|
+
function buildXml(nodes) {
|
|
256
|
+
const out = BUILDER.build(toOrdered(nodes));
|
|
257
|
+
if (typeof out !== "string") throw new Error("XMLBuilder did not return a string");
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function toOrdered(nodes) {
|
|
261
|
+
return nodes.map(toOrderedNode);
|
|
262
|
+
}
|
|
263
|
+
function attrsObject(attributes) {
|
|
264
|
+
const obj = {};
|
|
265
|
+
for (const a of attributes) obj[`@_${a.name}`] = a.value;
|
|
266
|
+
return obj;
|
|
267
|
+
}
|
|
268
|
+
function toOrderedNode(node) {
|
|
269
|
+
switch (node.type) {
|
|
270
|
+
case "text": return { "#text": node.value };
|
|
271
|
+
case "comment": return { __comment: [{ "#text": node.value }] };
|
|
272
|
+
case "cdata": return { __cdata: [{ "#text": node.value }] };
|
|
273
|
+
case "pi": return { [`?${node.target}`]: [{ "#text": node.content }] };
|
|
274
|
+
case "declaration": return {
|
|
275
|
+
"?xml": [{ "#text": "" }],
|
|
276
|
+
":@": attrsObject(node.attributes)
|
|
277
|
+
};
|
|
278
|
+
case "element": {
|
|
279
|
+
const obj = { [node.tag]: toOrdered(node.children) };
|
|
280
|
+
const attrs = attrsObject(node.attributes);
|
|
281
|
+
if (Object.keys(attrs).length > 0) obj[":@"] = attrs;
|
|
282
|
+
return obj;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/package-io/write.ts
|
|
288
|
+
const MIMETYPE_PART = "mimetype";
|
|
289
|
+
const MANIFEST_PART = "META-INF/manifest.xml";
|
|
290
|
+
function serializePackage(pkg) {
|
|
291
|
+
const remaining = new Map(Object.entries(pkg.parts));
|
|
292
|
+
const entries = [];
|
|
293
|
+
const mimetype = remaining.get(MIMETYPE_PART);
|
|
294
|
+
if (mimetype !== void 0) {
|
|
295
|
+
entries.push([MIMETYPE_PART, {
|
|
296
|
+
bytes: partToBytes(mimetype),
|
|
297
|
+
stored: true
|
|
298
|
+
}]);
|
|
299
|
+
remaining.delete(MIMETYPE_PART);
|
|
300
|
+
}
|
|
301
|
+
const manifest = remaining.get(MANIFEST_PART);
|
|
302
|
+
if (manifest !== void 0) {
|
|
303
|
+
entries.push([MANIFEST_PART, { bytes: partToBytes(manifest) }]);
|
|
304
|
+
remaining.delete(MANIFEST_PART);
|
|
305
|
+
}
|
|
306
|
+
for (const [path, part] of remaining) entries.push([path, { bytes: partToBytes(part) }]);
|
|
307
|
+
return zipPackage(entries);
|
|
308
|
+
}
|
|
309
|
+
function partToBytes(part) {
|
|
310
|
+
switch (part.kind) {
|
|
311
|
+
case "xml": return new TextEncoder().encode(buildXml(part.nodes));
|
|
312
|
+
case "binary": return base64ToBytes(part.base64);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/codec.ts
|
|
317
|
+
const xmlCodec = zod.z.codec(zod.z.string(), zod.z.array(XmlNodeSchema), {
|
|
318
|
+
decode: (xml) => parseXml(xml),
|
|
319
|
+
encode: (nodes) => buildXml(nodes)
|
|
320
|
+
});
|
|
321
|
+
const packageCodec = zod.z.codec(zod.z.instanceof(Uint8Array), PackageSchema, {
|
|
322
|
+
decode: (bytes) => parsePackage(bytes),
|
|
323
|
+
encode: (pkg) => serializePackage(pkg)
|
|
324
|
+
});
|
|
325
|
+
function decodePackage(bytes) {
|
|
326
|
+
return zod.z.decode(packageCodec, bytes);
|
|
327
|
+
}
|
|
328
|
+
function encodePackage(pkg) {
|
|
329
|
+
return zod.z.encode(packageCodec, pkg);
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/ns.ts
|
|
333
|
+
const ODF_NAMESPACES = Object.freeze({
|
|
334
|
+
office: "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
|
335
|
+
style: "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
|
336
|
+
text: "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
|
337
|
+
table: "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
|
|
338
|
+
draw: "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
|
|
339
|
+
fo: "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
|
340
|
+
svg: "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
|
|
341
|
+
xlink: "http://www.w3.org/1999/xlink",
|
|
342
|
+
dc: "http://purl.org/dc/elements/1.1/",
|
|
343
|
+
meta: "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
|
|
344
|
+
number: "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
|
|
345
|
+
chart: "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
|
|
346
|
+
dr3d: "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",
|
|
347
|
+
math: "http://www.w3.org/1998/Math/MathML",
|
|
348
|
+
form: "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
|
|
349
|
+
script: "urn:oasis:names:tc:opendocument:xmlns:script:1.0",
|
|
350
|
+
config: "urn:oasis:names:tc:opendocument:xmlns:config:1.0",
|
|
351
|
+
presentation: "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
|
|
352
|
+
smil: "urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0",
|
|
353
|
+
anim: "urn:oasis:names:tc:opendocument:xmlns:animation:1.0",
|
|
354
|
+
xforms: "http://www.w3.org/2002/xforms",
|
|
355
|
+
xsd: "http://www.w3.org/2001/XMLSchema",
|
|
356
|
+
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
|
357
|
+
manifest: "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
|
|
358
|
+
});
|
|
359
|
+
function xmlnsAttributes(prefixes) {
|
|
360
|
+
const attrs = {};
|
|
361
|
+
for (const prefix of prefixes) attrs[`xmlns:${prefix}`] = ODF_NAMESPACES[prefix];
|
|
362
|
+
return attrs;
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/media-type.ts
|
|
366
|
+
const ODF_MEDIA_TYPES = Object.freeze({
|
|
367
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
368
|
+
ott: "application/vnd.oasis.opendocument.text-template",
|
|
369
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
370
|
+
ots: "application/vnd.oasis.opendocument.spreadsheet-template",
|
|
371
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
372
|
+
otp: "application/vnd.oasis.opendocument.presentation-template",
|
|
373
|
+
odg: "application/vnd.oasis.opendocument.graphics",
|
|
374
|
+
otg: "application/vnd.oasis.opendocument.graphics-template",
|
|
375
|
+
odf: "application/vnd.oasis.opendocument.formula",
|
|
376
|
+
otf: "application/vnd.oasis.opendocument.formula-template",
|
|
377
|
+
odm: "application/vnd.oasis.opendocument.text-master",
|
|
378
|
+
otm: "application/vnd.oasis.opendocument.text-master-template",
|
|
379
|
+
odb: "application/vnd.oasis.opendocument.base"
|
|
380
|
+
});
|
|
381
|
+
function isOdfExtension(extension) {
|
|
382
|
+
return Object.hasOwn(ODF_MEDIA_TYPES, extension);
|
|
383
|
+
}
|
|
384
|
+
function mediaTypeForExtension(extension) {
|
|
385
|
+
const lower = extension.toLowerCase();
|
|
386
|
+
return isOdfExtension(lower) ? ODF_MEDIA_TYPES[lower] : void 0;
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/image/sniff.ts
|
|
390
|
+
const PNG_SIGNATURE = [
|
|
391
|
+
137,
|
|
392
|
+
80,
|
|
393
|
+
78,
|
|
394
|
+
71,
|
|
395
|
+
13,
|
|
396
|
+
10,
|
|
397
|
+
26,
|
|
398
|
+
10
|
|
399
|
+
];
|
|
400
|
+
const JPEG_SIGNATURE = [
|
|
401
|
+
255,
|
|
402
|
+
216,
|
|
403
|
+
255
|
|
404
|
+
];
|
|
405
|
+
function startsWith(bytes, signature) {
|
|
406
|
+
if (bytes.length < signature.length) return false;
|
|
407
|
+
for (let i = 0; i < signature.length; i++) if (bytes[i] !== signature[i]) return false;
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
function sniffImageFormat(bytes) {
|
|
411
|
+
if (startsWith(bytes, PNG_SIGNATURE)) return "png";
|
|
412
|
+
if (startsWith(bytes, JPEG_SIGNATURE)) return "jpeg";
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/mimetype.ts
|
|
416
|
+
function readMimetype(pkg) {
|
|
417
|
+
const part = pkg.parts[MIMETYPE_PART];
|
|
418
|
+
if (part?.kind !== "binary") return;
|
|
419
|
+
return new TextDecoder("utf-8").decode(base64ToBytes(part.base64));
|
|
420
|
+
}
|
|
421
|
+
function writeMimetype(pkg, mediaType) {
|
|
422
|
+
pkg.parts[MIMETYPE_PART] = {
|
|
423
|
+
kind: "binary",
|
|
424
|
+
base64: bytesToBase64(new TextEncoder().encode(mediaType))
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/xml/fragment.ts
|
|
429
|
+
function el(tag, attrs = {}, children = []) {
|
|
430
|
+
return {
|
|
431
|
+
type: "element",
|
|
432
|
+
tag,
|
|
433
|
+
attributes: Object.entries(attrs).map(([name, value]) => ({
|
|
434
|
+
name,
|
|
435
|
+
value
|
|
436
|
+
})),
|
|
437
|
+
children
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function txt(value) {
|
|
441
|
+
return {
|
|
442
|
+
type: "text",
|
|
443
|
+
value
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/xml/entities.ts
|
|
448
|
+
function encodeXmlText(value) {
|
|
449
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/manifest.ts
|
|
453
|
+
const ManifestEntrySchema = zod.z.object({
|
|
454
|
+
fullPath: zod.z.string(),
|
|
455
|
+
mediaType: zod.z.string(),
|
|
456
|
+
version: zod.z.string().optional()
|
|
457
|
+
});
|
|
458
|
+
const ManifestSchema = zod.z.object({
|
|
459
|
+
version: zod.z.string(),
|
|
460
|
+
entries: zod.z.array(ManifestEntrySchema)
|
|
461
|
+
});
|
|
462
|
+
const ManifestProblemSchema = zod.z.object({
|
|
463
|
+
severity: zod.z.enum(["error", "warning"]),
|
|
464
|
+
message: zod.z.string(),
|
|
465
|
+
path: zod.z.string().optional()
|
|
466
|
+
});
|
|
467
|
+
const DEFAULT_MANIFEST_VERSION = "1.3";
|
|
468
|
+
const STANDARD_XML_PART_NAMES = /* @__PURE__ */ new Set([
|
|
469
|
+
"content.xml",
|
|
470
|
+
"styles.xml",
|
|
471
|
+
"meta.xml",
|
|
472
|
+
"settings.xml"
|
|
473
|
+
]);
|
|
474
|
+
function findChildElement(nodes, tag) {
|
|
475
|
+
for (const node of nodes) if (node.type === "element" && node.tag === tag) return node;
|
|
476
|
+
}
|
|
477
|
+
function attrValue(element, name) {
|
|
478
|
+
return element.attributes.find((attribute) => attribute.name === name)?.value;
|
|
479
|
+
}
|
|
480
|
+
function readManifest(pkg) {
|
|
481
|
+
const part = pkg.parts[MANIFEST_PART];
|
|
482
|
+
if (part?.kind !== "xml") throw new Error(`package has no ${MANIFEST_PART} XML part to read`);
|
|
483
|
+
const root = findChildElement(part.nodes, "manifest:manifest");
|
|
484
|
+
if (root === void 0) throw new Error(`${MANIFEST_PART} has no manifest:manifest root element`);
|
|
485
|
+
const version = attrValue(root, "manifest:version");
|
|
486
|
+
if (version === void 0) throw new Error(`${MANIFEST_PART}'s manifest:manifest root is missing the required manifest:version attribute`);
|
|
487
|
+
const entries = [];
|
|
488
|
+
for (const child of root.children) {
|
|
489
|
+
if (child.type !== "element" || child.tag !== "manifest:file-entry") continue;
|
|
490
|
+
const fullPath = attrValue(child, "manifest:full-path");
|
|
491
|
+
const mediaType = attrValue(child, "manifest:media-type");
|
|
492
|
+
if (fullPath === void 0 || mediaType === void 0) throw new Error(`${MANIFEST_PART} has a manifest:file-entry missing manifest:full-path or manifest:media-type`);
|
|
493
|
+
const entryVersion = attrValue(child, "manifest:version");
|
|
494
|
+
entries.push(entryVersion === void 0 ? {
|
|
495
|
+
fullPath,
|
|
496
|
+
mediaType
|
|
497
|
+
} : {
|
|
498
|
+
fullPath,
|
|
499
|
+
mediaType,
|
|
500
|
+
version: entryVersion
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
version,
|
|
505
|
+
entries
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function subdocumentDirectories(partPaths) {
|
|
509
|
+
const dirs = [];
|
|
510
|
+
for (const path of partPaths) if (path.endsWith("/content.xml")) dirs.push(path.slice(0, path.length - 11));
|
|
511
|
+
return dirs;
|
|
512
|
+
}
|
|
513
|
+
function resolvePartMediaType(path, bytes, overrides) {
|
|
514
|
+
const override = overrides?.[path];
|
|
515
|
+
if (override !== void 0) return override;
|
|
516
|
+
const baseName = path.slice(path.lastIndexOf("/") + 1);
|
|
517
|
+
if (STANDARD_XML_PART_NAMES.has(baseName)) return "text/xml";
|
|
518
|
+
const dotIndex = baseName.lastIndexOf(".");
|
|
519
|
+
const extension = dotIndex === -1 ? "" : baseName.slice(dotIndex + 1);
|
|
520
|
+
const byExtension = extension === "" ? void 0 : mediaTypeForExtension(extension);
|
|
521
|
+
if (byExtension !== void 0) return byExtension;
|
|
522
|
+
if (bytes !== void 0) {
|
|
523
|
+
const sniffed = sniffImageFormat(bytes);
|
|
524
|
+
if (sniffed === "png") return "image/png";
|
|
525
|
+
if (sniffed === "jpeg") return "image/jpeg";
|
|
526
|
+
}
|
|
527
|
+
return "";
|
|
528
|
+
}
|
|
529
|
+
function buildManifest(pkg, options = {}) {
|
|
530
|
+
const version = options.version ?? DEFAULT_MANIFEST_VERSION;
|
|
531
|
+
const documentMediaType = options.documentMediaType ?? readMimetype(pkg);
|
|
532
|
+
if (documentMediaType === void 0) throw new Error("buildManifest: package has no \"mimetype\" part and no documentMediaType override was supplied -- the manifest root entry requires a known document media type");
|
|
533
|
+
const entries = [{
|
|
534
|
+
fullPath: "/",
|
|
535
|
+
mediaType: documentMediaType,
|
|
536
|
+
version
|
|
537
|
+
}];
|
|
538
|
+
const partPaths = Object.keys(pkg.parts);
|
|
539
|
+
for (const dir of new Set(subdocumentDirectories(partPaths))) entries.push({
|
|
540
|
+
fullPath: dir,
|
|
541
|
+
mediaType: resolvePartMediaType(dir, void 0, options.mediaTypeOverrides)
|
|
542
|
+
});
|
|
543
|
+
for (const [path, part] of Object.entries(pkg.parts)) {
|
|
544
|
+
if (path === "mimetype" || path === "META-INF/manifest.xml") continue;
|
|
545
|
+
const bytes = part.kind === "binary" ? base64ToBytes(part.base64) : void 0;
|
|
546
|
+
entries.push({
|
|
547
|
+
fullPath: path,
|
|
548
|
+
mediaType: resolvePartMediaType(path, bytes, options.mediaTypeOverrides)
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
version,
|
|
553
|
+
entries
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function buildManifestNodes(manifest) {
|
|
557
|
+
const fileEntries = manifest.entries.map((entry) => {
|
|
558
|
+
const attrs = { "manifest:full-path": encodeXmlText(entry.fullPath) };
|
|
559
|
+
if (entry.version !== void 0) attrs["manifest:version"] = encodeXmlText(entry.version);
|
|
560
|
+
attrs["manifest:media-type"] = encodeXmlText(entry.mediaType);
|
|
561
|
+
return el("manifest:file-entry", attrs);
|
|
562
|
+
});
|
|
563
|
+
return [{
|
|
564
|
+
type: "declaration",
|
|
565
|
+
attributes: [{
|
|
566
|
+
name: "version",
|
|
567
|
+
value: "1.0"
|
|
568
|
+
}, {
|
|
569
|
+
name: "encoding",
|
|
570
|
+
value: "UTF-8"
|
|
571
|
+
}]
|
|
572
|
+
}, el("manifest:manifest", {
|
|
573
|
+
...xmlnsAttributes(["manifest"]),
|
|
574
|
+
"manifest:version": encodeXmlText(manifest.version)
|
|
575
|
+
}, fileEntries)];
|
|
576
|
+
}
|
|
577
|
+
function writeManifest(pkg, manifest) {
|
|
578
|
+
pkg.parts[MANIFEST_PART] = {
|
|
579
|
+
kind: "xml",
|
|
580
|
+
nodes: buildManifestNodes(manifest)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function syncManifest(pkg, options) {
|
|
584
|
+
writeManifest(pkg, buildManifest(pkg, options));
|
|
585
|
+
}
|
|
586
|
+
function validateManifest(pkg) {
|
|
587
|
+
const problems = [];
|
|
588
|
+
const manifestPart = pkg.parts[MANIFEST_PART];
|
|
589
|
+
if (manifestPart === void 0) {
|
|
590
|
+
problems.push({
|
|
591
|
+
severity: "error",
|
|
592
|
+
message: `package has no ${MANIFEST_PART} part`
|
|
593
|
+
});
|
|
594
|
+
return problems;
|
|
595
|
+
}
|
|
596
|
+
if (manifestPart.kind !== "xml") {
|
|
597
|
+
problems.push({
|
|
598
|
+
severity: "error",
|
|
599
|
+
message: `${MANIFEST_PART} part is not XML`
|
|
600
|
+
});
|
|
601
|
+
return problems;
|
|
602
|
+
}
|
|
603
|
+
const root = findChildElement(manifestPart.nodes, "manifest:manifest");
|
|
604
|
+
if (root === void 0) {
|
|
605
|
+
problems.push({
|
|
606
|
+
severity: "error",
|
|
607
|
+
message: `${MANIFEST_PART} has no manifest:manifest root element`
|
|
608
|
+
});
|
|
609
|
+
return problems;
|
|
610
|
+
}
|
|
611
|
+
let manifest;
|
|
612
|
+
try {
|
|
613
|
+
manifest = readManifest(pkg);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
problems.push({
|
|
616
|
+
severity: "error",
|
|
617
|
+
message: `failed to parse ${MANIFEST_PART}: ${error instanceof Error ? error.message : String(error)}`
|
|
618
|
+
});
|
|
619
|
+
return problems;
|
|
620
|
+
}
|
|
621
|
+
const rootEntry = manifest.entries.find((entry) => entry.fullPath === "/");
|
|
622
|
+
if (rootEntry === void 0) problems.push({
|
|
623
|
+
severity: "error",
|
|
624
|
+
message: "manifest has no root (\"/\") entry"
|
|
625
|
+
});
|
|
626
|
+
else {
|
|
627
|
+
const documentMediaType = readMimetype(pkg);
|
|
628
|
+
if (documentMediaType !== void 0 && documentMediaType !== rootEntry.mediaType) problems.push({
|
|
629
|
+
severity: "error",
|
|
630
|
+
message: `manifest root entry media type "${rootEntry.mediaType}" does not match the mimetype part's media type "${documentMediaType}"`,
|
|
631
|
+
path: "/"
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
const partPaths = new Set(Object.keys(pkg.parts).filter((path) => path !== "mimetype" && path !== "META-INF/manifest.xml"));
|
|
635
|
+
const manifestPaths = new Set(manifest.entries.map((entry) => entry.fullPath));
|
|
636
|
+
for (const entry of manifest.entries) {
|
|
637
|
+
if (entry.fullPath === "/" || entry.fullPath.endsWith("/")) continue;
|
|
638
|
+
if (!partPaths.has(entry.fullPath)) problems.push({
|
|
639
|
+
severity: "warning",
|
|
640
|
+
message: `manifest lists "${entry.fullPath}" but the package has no such part`,
|
|
641
|
+
path: entry.fullPath
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
for (const path of partPaths) if (!manifestPaths.has(path)) problems.push({
|
|
645
|
+
severity: "warning",
|
|
646
|
+
message: `package has part "${path}" not listed in the manifest`,
|
|
647
|
+
path
|
|
648
|
+
});
|
|
649
|
+
for (const child of root.children) {
|
|
650
|
+
if (child.type !== "element" || child.tag !== "manifest:file-entry") continue;
|
|
651
|
+
if (!child.children.some((grandchild) => grandchild.type === "element" && grandchild.tag === "manifest:encryption-data")) continue;
|
|
652
|
+
const fullPath = attrValue(child, "manifest:full-path");
|
|
653
|
+
if (fullPath === void 0) continue;
|
|
654
|
+
problems.push({
|
|
655
|
+
severity: "warning",
|
|
656
|
+
message: `entry "${fullPath}" carries manifest:encryption-data -- odf.js does not implement ODF encryption/decryption`,
|
|
657
|
+
path: fullPath
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return problems;
|
|
661
|
+
}
|
|
662
|
+
function setDocumentMediaType(pkg, mediaType, version = DEFAULT_MANIFEST_VERSION) {
|
|
663
|
+
writeMimetype(pkg, mediaType);
|
|
664
|
+
const rootEntry = {
|
|
665
|
+
fullPath: "/",
|
|
666
|
+
mediaType,
|
|
667
|
+
version
|
|
668
|
+
};
|
|
669
|
+
if (pkg.parts["META-INF/manifest.xml"] === void 0) {
|
|
670
|
+
writeManifest(pkg, {
|
|
671
|
+
version,
|
|
672
|
+
entries: [rootEntry]
|
|
673
|
+
});
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const existing = readManifest(pkg);
|
|
677
|
+
const rootIndex = existing.entries.findIndex((entry) => entry.fullPath === "/");
|
|
678
|
+
writeManifest(pkg, {
|
|
679
|
+
version,
|
|
680
|
+
entries: rootIndex === -1 ? [rootEntry, ...existing.entries] : existing.entries.map((entry, index) => index === rootIndex ? rootEntry : entry)
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
684
|
+
exports.AttributeSchema = AttributeSchema;
|
|
685
|
+
exports.BinaryPartSchema = BinaryPartSchema;
|
|
686
|
+
exports.MANIFEST_PART = MANIFEST_PART;
|
|
687
|
+
exports.MIMETYPE_PART = MIMETYPE_PART;
|
|
688
|
+
exports.ManifestEntrySchema = ManifestEntrySchema;
|
|
689
|
+
exports.ManifestProblemSchema = ManifestProblemSchema;
|
|
690
|
+
exports.ManifestSchema = ManifestSchema;
|
|
691
|
+
exports.ODF_MEDIA_TYPES = ODF_MEDIA_TYPES;
|
|
692
|
+
exports.ODF_NAMESPACES = ODF_NAMESPACES;
|
|
693
|
+
exports.PackageSchema = PackageSchema;
|
|
694
|
+
exports.PartSchema = PartSchema;
|
|
695
|
+
exports.XmlCdataSchema = XmlCdataSchema;
|
|
696
|
+
exports.XmlCommentSchema = XmlCommentSchema;
|
|
697
|
+
exports.XmlDeclarationSchema = XmlDeclarationSchema;
|
|
698
|
+
exports.XmlElementSchema = XmlElementSchema;
|
|
699
|
+
exports.XmlNodeSchema = XmlNodeSchema;
|
|
700
|
+
exports.XmlPartSchema = XmlPartSchema;
|
|
701
|
+
exports.XmlPiSchema = XmlPiSchema;
|
|
702
|
+
exports.XmlTextSchema = XmlTextSchema;
|
|
703
|
+
exports.base64ToBytes = base64ToBytes;
|
|
704
|
+
exports.buildManifest = buildManifest;
|
|
705
|
+
exports.buildXml = buildXml;
|
|
706
|
+
exports.bytesToBase64 = bytesToBase64;
|
|
707
|
+
exports.decodePackage = decodePackage;
|
|
708
|
+
exports.el = el;
|
|
709
|
+
exports.encodePackage = encodePackage;
|
|
710
|
+
exports.encodeXmlText = encodeXmlText;
|
|
711
|
+
exports.isXmlNode = isXmlNode;
|
|
712
|
+
exports.mediaTypeForExtension = mediaTypeForExtension;
|
|
713
|
+
exports.packageCodec = packageCodec;
|
|
714
|
+
exports.parsePackage = parsePackage;
|
|
715
|
+
exports.parseXml = parseXml;
|
|
716
|
+
exports.readManifest = readManifest;
|
|
717
|
+
exports.readMimetype = readMimetype;
|
|
718
|
+
exports.serializePackage = serializePackage;
|
|
719
|
+
exports.setDocumentMediaType = setDocumentMediaType;
|
|
720
|
+
exports.sniffImageFormat = sniffImageFormat;
|
|
721
|
+
exports.syncManifest = syncManifest;
|
|
722
|
+
exports.txt = txt;
|
|
723
|
+
exports.unzipPackage = unzipPackage;
|
|
724
|
+
exports.validateManifest = validateManifest;
|
|
725
|
+
exports.writeManifest = writeManifest;
|
|
726
|
+
exports.writeMimetype = writeMimetype;
|
|
727
|
+
exports.xmlCodec = xmlCodec;
|
|
728
|
+
exports.xmlnsAttributes = xmlnsAttributes;
|
|
729
|
+
exports.zipPackage = zipPackage;
|