web-doc 0.6.2 → 0.8.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/THIRD_PARTY_NOTICES.md +33 -1
- package/dist/adapters/docx-images.d.ts +13 -4
- package/dist/adapters/docx-images.js +3 -0
- package/dist/adapters/docx-paragraphs.d.ts +65 -0
- package/dist/adapters/docx-paragraphs.js +165 -0
- package/dist/adapters/docx-prepass.d.ts +17 -0
- package/dist/adapters/docx-prepass.js +230 -0
- package/dist/adapters/office.d.ts +31 -5
- package/dist/adapters/office.js +60 -29
- package/dist/adapters/pdf.d.ts +9 -0
- package/dist/adapters/pdf.js +10 -0
- package/dist/assets/pdfium/pdfium.wasm +0 -0
- package/dist/contracts.d.ts +53 -2
- package/dist/edit/ai/outline.d.ts +47 -0
- package/dist/edit/ai/outline.js +338 -0
- package/dist/edit/ai/targets.d.ts +16 -0
- package/dist/edit/ai/targets.js +309 -0
- package/dist/edit/ai/tools.d.ts +28 -0
- package/dist/edit/ai/tools.js +605 -0
- package/dist/edit/ai/types.d.ts +175 -0
- package/dist/edit/ai/types.js +1 -0
- package/dist/edit/assets.d.ts +23 -0
- package/dist/edit/assets.js +75 -0
- package/dist/edit/docx/elements.d.ts +6 -0
- package/dist/edit/docx/elements.js +90 -0
- package/dist/edit/docx/engine.d.ts +57 -0
- package/dist/edit/docx/engine.js +547 -0
- package/dist/edit/docx/handlers.d.ts +3 -0
- package/dist/edit/docx/handlers.js +15 -0
- package/dist/edit/docx/ids.d.ts +45 -0
- package/dist/edit/docx/ids.js +101 -0
- package/dist/edit/docx/model.d.ts +94 -0
- package/dist/edit/docx/model.js +350 -0
- package/dist/edit/docx/operations.d.ts +47 -0
- package/dist/edit/docx/operations.js +3 -0
- package/dist/edit/docx/provider.d.ts +16 -0
- package/dist/edit/docx/provider.js +37 -0
- package/dist/edit/docx/schemas.d.ts +6 -0
- package/dist/edit/docx/schemas.js +192 -0
- package/dist/edit/docx/session.d.ts +48 -0
- package/dist/edit/docx/session.js +467 -0
- package/dist/edit/docx/structure-ops.d.ts +6 -0
- package/dist/edit/docx/structure-ops.js +529 -0
- package/dist/edit/docx/style.d.ts +45 -0
- package/dist/edit/docx/style.js +375 -0
- package/dist/edit/docx/table-ops.d.ts +4 -0
- package/dist/edit/docx/table-ops.js +241 -0
- package/dist/edit/docx/text-ops.d.ts +40 -0
- package/dist/edit/docx/text-ops.js +491 -0
- package/dist/edit/docx/text.d.ts +34 -0
- package/dist/edit/docx/text.js +284 -0
- package/dist/edit/docx/tracked.d.ts +52 -0
- package/dist/edit/docx/tracked.js +347 -0
- package/dist/edit/docx/types.d.ts +206 -0
- package/dist/edit/docx/types.js +1 -0
- package/dist/edit/docx/write.d.ts +64 -0
- package/dist/edit/docx/write.js +375 -0
- package/dist/edit/engine.d.ts +128 -0
- package/dist/edit/engine.js +1 -0
- package/dist/edit/history.d.ts +77 -0
- package/dist/edit/history.js +123 -0
- package/dist/edit/ooxml/names.d.ts +20 -0
- package/dist/edit/ooxml/names.js +61 -0
- package/dist/edit/ooxml/opc.d.ts +50 -0
- package/dist/edit/ooxml/opc.js +150 -0
- package/dist/edit/ooxml/package.d.ts +82 -0
- package/dist/edit/ooxml/package.js +233 -0
- package/dist/edit/ooxml/patch.d.ts +51 -0
- package/dist/edit/ooxml/patch.js +250 -0
- package/dist/edit/ooxml/transaction.d.ts +39 -0
- package/dist/edit/ooxml/transaction.js +316 -0
- package/dist/edit/ooxml/worker.d.ts +8 -0
- package/dist/edit/ooxml/worker.js +12 -0
- package/dist/edit/ooxml/writer.d.ts +21 -0
- package/dist/edit/ooxml/writer.js +187 -0
- package/dist/edit/ooxml/xml.d.ts +74 -0
- package/dist/edit/ooxml/xml.js +451 -0
- package/dist/edit/ooxml/zip.d.ts +54 -0
- package/dist/edit/ooxml/zip.js +280 -0
- package/dist/edit/operations.d.ts +19 -0
- package/dist/edit/operations.js +137 -0
- package/dist/edit/pdf/engine/compact.d.ts +6 -0
- package/dist/edit/pdf/engine/compact.js +442 -0
- package/dist/edit/pdf/engine/document.d.ts +95 -0
- package/dist/edit/pdf/engine/document.js +868 -0
- package/dist/edit/pdf/engine/elements.d.ts +45 -0
- package/dist/edit/pdf/engine/elements.js +313 -0
- package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
- package/dist/edit/pdf/engine/existing-text.js +424 -0
- package/dist/edit/pdf/engine/fonts.d.ts +78 -0
- package/dist/edit/pdf/engine/fonts.js +466 -0
- package/dist/edit/pdf/engine/geometry.d.ts +36 -0
- package/dist/edit/pdf/engine/geometry.js +97 -0
- package/dist/edit/pdf/engine/handler.d.ts +21 -0
- package/dist/edit/pdf/engine/handler.js +108 -0
- package/dist/edit/pdf/engine/images.d.ts +33 -0
- package/dist/edit/pdf/engine/images.js +188 -0
- package/dist/edit/pdf/engine/layout.d.ts +23 -0
- package/dist/edit/pdf/engine/layout.js +177 -0
- package/dist/edit/pdf/engine/operations.d.ts +67 -0
- package/dist/edit/pdf/engine/operations.js +3 -0
- package/dist/edit/pdf/engine/pages.d.ts +6 -0
- package/dist/edit/pdf/engine/pages.js +98 -0
- package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
- package/dist/edit/pdf/engine/pdfium.js +249 -0
- package/dist/edit/pdf/engine/shapes.d.ts +4 -0
- package/dist/edit/pdf/engine/shapes.js +183 -0
- package/dist/edit/pdf/engine/tables.d.ts +44 -0
- package/dist/edit/pdf/engine/tables.js +315 -0
- package/dist/edit/pdf/engine/text-box.d.ts +71 -0
- package/dist/edit/pdf/engine/text-box.js +317 -0
- package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
- package/dist/edit/pdf/engine/text-layout.js +67 -0
- package/dist/edit/pdf/engine/transform.d.ts +5 -0
- package/dist/edit/pdf/engine/transform.js +137 -0
- package/dist/edit/pdf/provider.d.ts +35 -0
- package/dist/edit/pdf/provider.js +92 -0
- package/dist/edit/pdf/range-map.d.ts +16 -0
- package/dist/edit/pdf/range-map.js +107 -0
- package/dist/edit/pdf/schemas.d.ts +11 -0
- package/dist/edit/pdf/schemas.js +291 -0
- package/dist/edit/pdf/selection.d.ts +5 -0
- package/dist/edit/pdf/selection.js +152 -0
- package/dist/edit/pdf/session.d.ts +63 -0
- package/dist/edit/pdf/session.js +312 -0
- package/dist/edit/pdf/types.d.ts +336 -0
- package/dist/edit/pdf/types.js +1 -0
- package/dist/edit/pptx/elements.d.ts +74 -0
- package/dist/edit/pptx/elements.js +301 -0
- package/dist/edit/pptx/engine.d.ts +37 -0
- package/dist/edit/pptx/engine.js +466 -0
- package/dist/edit/pptx/geometry.d.ts +72 -0
- package/dist/edit/pptx/geometry.js +193 -0
- package/dist/edit/pptx/handler.d.ts +7 -0
- package/dist/edit/pptx/handler.js +99 -0
- package/dist/edit/pptx/handlers.d.ts +3 -0
- package/dist/edit/pptx/handlers.js +21 -0
- package/dist/edit/pptx/image-table-ops.d.ts +5 -0
- package/dist/edit/pptx/image-table-ops.js +233 -0
- package/dist/edit/pptx/model.d.ts +69 -0
- package/dist/edit/pptx/model.js +170 -0
- package/dist/edit/pptx/operations.d.ts +50 -0
- package/dist/edit/pptx/operations.js +11 -0
- package/dist/edit/pptx/provider.d.ts +17 -0
- package/dist/edit/pptx/provider.js +40 -0
- package/dist/edit/pptx/schemas.d.ts +6 -0
- package/dist/edit/pptx/schemas.js +220 -0
- package/dist/edit/pptx/session.d.ts +54 -0
- package/dist/edit/pptx/session.js +145 -0
- package/dist/edit/pptx/shape-ops.d.ts +10 -0
- package/dist/edit/pptx/shape-ops.js +486 -0
- package/dist/edit/pptx/slide-ops.d.ts +6 -0
- package/dist/edit/pptx/slide-ops.js +353 -0
- package/dist/edit/pptx/style.d.ts +31 -0
- package/dist/edit/pptx/style.js +181 -0
- package/dist/edit/pptx/text-ops.d.ts +13 -0
- package/dist/edit/pptx/text-ops.js +267 -0
- package/dist/edit/pptx/text-write.d.ts +67 -0
- package/dist/edit/pptx/text-write.js +293 -0
- package/dist/edit/pptx/text.d.ts +41 -0
- package/dist/edit/pptx/text.js +99 -0
- package/dist/edit/pptx/types.d.ts +232 -0
- package/dist/edit/pptx/types.js +1 -0
- package/dist/edit/schema.d.ts +11 -0
- package/dist/edit/schema.js +275 -0
- package/dist/edit/session.d.ts +77 -0
- package/dist/edit/session.js +943 -0
- package/dist/edit/sessions.d.ts +8 -0
- package/dist/edit/sessions.js +1 -0
- package/dist/edit/types.d.ts +290 -0
- package/dist/edit/types.js +1 -0
- package/dist/edit/worker-engine.d.ts +31 -0
- package/dist/edit/worker-engine.js +91 -0
- package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/fonts/manifest.json +6 -1
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/fonts.d.ts +2 -0
- package/dist/fonts.js +4 -0
- package/dist/fuzzy-alignment.d.ts +11 -4
- package/dist/fuzzy-alignment.js +3 -9
- package/dist/headless.d.ts +5 -1
- package/dist/headless.js +8 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +16 -2
- package/dist/limits.js +6 -0
- package/dist/ooxml-edit-worker.d.ts +1 -0
- package/dist/ooxml-edit-worker.js +8 -0
- package/dist/pdf-edit-worker.d.ts +1 -0
- package/dist/pdf-edit-worker.js +35 -0
- package/dist/spreadsheet-viewport.d.ts +5 -0
- package/dist/spreadsheet-viewport.js +11 -0
- package/dist/ui.js +7 -0
- package/dist/viewer.d.ts +6 -0
- package/dist/viewer.js +293 -19
- package/dist/viewport.d.ts +13 -0
- package/dist/viewport.js +126 -1
- package/dist/worker-protocol.d.ts +39 -2
- package/dist/workers/fuzzy-search-worker.js +1 -1
- package/dist/workers/ooxml-edit-worker.js +9212 -0
- package/dist/workers/pdf-edit-worker.js +10847 -0
- package/package.json +3 -3
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { ViewerError } from "../../errors.js";
|
|
2
|
+
import { extensionOf, partKey, partNameOf, relationshipsPartOf, } from "./names.js";
|
|
3
|
+
import { CONTENT_TYPES_NAMESPACE, extensionForMime, IMAGE_RELATIONSHIP_TYPE, parseContentTypes, parseRelationships, RELATIONSHIPS_NAMESPACE, sourceOfRelationshipsPart, } from "./opc.js";
|
|
4
|
+
import { applyPatches, patches } from "./patch.js";
|
|
5
|
+
import { encodePart, escapeAttribute, scanXml } from "./xml.js";
|
|
6
|
+
const CONTENT_TYPES = "/[Content_Types].xml";
|
|
7
|
+
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
8
|
+
export class PackageTransaction {
|
|
9
|
+
#pkg;
|
|
10
|
+
#revision;
|
|
11
|
+
#patches = new Map();
|
|
12
|
+
#set = new Map();
|
|
13
|
+
#removed = new Set();
|
|
14
|
+
#relationshipAdds = new Map();
|
|
15
|
+
#relationshipRemoves = new Map();
|
|
16
|
+
#relationshipSets = new Map();
|
|
17
|
+
#mediaByHash = new Map();
|
|
18
|
+
#committed = false;
|
|
19
|
+
constructor(pkg) {
|
|
20
|
+
this.#pkg = pkg;
|
|
21
|
+
this.#revision = pkg.revision;
|
|
22
|
+
}
|
|
23
|
+
/** Patches of one scanned part; the scan must be of the current revision. */
|
|
24
|
+
patch(part, items) {
|
|
25
|
+
this.#open();
|
|
26
|
+
if (part.revision !== this.#revision)
|
|
27
|
+
throw new ViewerError("invalid-patch", `Part ${part.name} was scanned at another revision`, {
|
|
28
|
+
details: {
|
|
29
|
+
part: part.name,
|
|
30
|
+
scannedAt: part.revision,
|
|
31
|
+
current: this.#revision,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const key = partKey(part.name);
|
|
35
|
+
if (this.#set.has(key) || this.#removed.has(key))
|
|
36
|
+
throw new ViewerError("invalid-patch", `Part ${part.name} is replaced or removed in this transaction`, {
|
|
37
|
+
details: { part: part.name },
|
|
38
|
+
});
|
|
39
|
+
const existing = this.#patches.get(key);
|
|
40
|
+
if (existing)
|
|
41
|
+
existing.items.push(...items);
|
|
42
|
+
else
|
|
43
|
+
this.#patches.set(key, { part, items: [...items] });
|
|
44
|
+
}
|
|
45
|
+
/** Adds or replaces a part; `contentType` registers it unless a Default covers the extension. */
|
|
46
|
+
setPart(name, bytes, contentType) {
|
|
47
|
+
this.#open();
|
|
48
|
+
const partName = partNameOf(name);
|
|
49
|
+
const key = partKey(partName);
|
|
50
|
+
if (this.#patches.has(key))
|
|
51
|
+
throw new ViewerError("invalid-patch", `Part ${partName} is patched in this transaction`, {
|
|
52
|
+
details: { part: partName },
|
|
53
|
+
});
|
|
54
|
+
this.#removed.delete(key);
|
|
55
|
+
this.#set.set(key, {
|
|
56
|
+
name: partName,
|
|
57
|
+
bytes: bytes.slice(),
|
|
58
|
+
...(contentType ? { contentType } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
removePart(name) {
|
|
62
|
+
this.#open();
|
|
63
|
+
const key = partKey(name);
|
|
64
|
+
this.#patches.delete(key);
|
|
65
|
+
this.#set.delete(key);
|
|
66
|
+
this.#removed.add(key);
|
|
67
|
+
}
|
|
68
|
+
/** Allocates the next free rId of the source's .rels part, pending adds included. */
|
|
69
|
+
async addRelationship(sourcePart, type, target, mode = "Internal") {
|
|
70
|
+
this.#open();
|
|
71
|
+
const source = sourcePart === "/" ? "/" : partNameOf(sourcePart);
|
|
72
|
+
const set = await this.#relationships(source);
|
|
73
|
+
const pending = this.#relationshipAdds.get(source) ?? [];
|
|
74
|
+
const taken = new Set(pending.map((add) => add.id));
|
|
75
|
+
const id = set.nextId(taken);
|
|
76
|
+
pending.push({ id, type, target, mode });
|
|
77
|
+
this.#relationshipAdds.set(source, pending);
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
removeRelationship(sourcePart, id) {
|
|
81
|
+
this.#open();
|
|
82
|
+
const source = sourcePart === "/" ? "/" : partNameOf(sourcePart);
|
|
83
|
+
const pending = this.#relationshipAdds.get(source);
|
|
84
|
+
if (pending) {
|
|
85
|
+
const index = pending.findIndex((add) => add.id === id);
|
|
86
|
+
if (index >= 0) {
|
|
87
|
+
pending.splice(index, 1);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const removes = this.#relationshipRemoves.get(source) ?? new Set();
|
|
92
|
+
removes.add(id);
|
|
93
|
+
this.#relationshipRemoves.set(source, removes);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stores media once per distinct content under `folder`, registers the
|
|
97
|
+
* extension's content type when missing, and relates it to `sourcePart`.
|
|
98
|
+
*/
|
|
99
|
+
async addMedia(sourcePart, folder, bytes, mimeType, relationshipType = IMAGE_RELATIONSHIP_TYPE) {
|
|
100
|
+
this.#open();
|
|
101
|
+
const extension = extensionForMime(mimeType);
|
|
102
|
+
const hash = `${folder}|${extension}|${await sha256(bytes)}`;
|
|
103
|
+
let part = this.#mediaByHash.get(hash);
|
|
104
|
+
if (!part) {
|
|
105
|
+
part = this.uniquePartName(`${partNameOf(folder).replace(/\/?$/, "/")}image`, `.${extension}`);
|
|
106
|
+
this.#mediaByHash.set(hash, part);
|
|
107
|
+
const types = await this.#contentTypes();
|
|
108
|
+
const needsDefault = !types.defaults.has(extension) &&
|
|
109
|
+
![...this.#set.values()].some((entry) => entry.contentType && extensionOf(entry.name) === extension);
|
|
110
|
+
this.setPart(part, bytes, needsDefault ? mimeType : undefined);
|
|
111
|
+
}
|
|
112
|
+
const source = sourcePart === "/" ? "/" : partNameOf(sourcePart);
|
|
113
|
+
const rId = await this.addRelationship(source, relationshipType, relativeTarget(source, part));
|
|
114
|
+
return { part, rId };
|
|
115
|
+
}
|
|
116
|
+
/** A part name that does not exist yet, in the package or in this transaction. */
|
|
117
|
+
uniquePartName(prefix, extension) {
|
|
118
|
+
const base = partNameOf(prefix);
|
|
119
|
+
for (let n = 1;; n += 1) {
|
|
120
|
+
const candidate = `${base}${n}${extension}`;
|
|
121
|
+
const key = partKey(candidate);
|
|
122
|
+
if (this.#removed.has(key))
|
|
123
|
+
return candidate;
|
|
124
|
+
if (!this.#pkg.has(candidate) && !this.#set.has(key))
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Applies everything or nothing; returns what changed. */
|
|
129
|
+
async commit(signal) {
|
|
130
|
+
this.#open();
|
|
131
|
+
if (signal?.aborted)
|
|
132
|
+
throw new ViewerError("aborted", "The transaction was aborted");
|
|
133
|
+
if (this.#pkg.revision !== this.#revision)
|
|
134
|
+
throw new ViewerError("edit-conflict", "The package changed since the transaction started", {
|
|
135
|
+
details: { expected: this.#revision, actual: this.#pkg.revision },
|
|
136
|
+
});
|
|
137
|
+
const set = new Map();
|
|
138
|
+
const remove = new Set();
|
|
139
|
+
const next = this.#revision + 1;
|
|
140
|
+
// 1. Patched parts, verified by re-scan.
|
|
141
|
+
for (const { part, items } of this.#patches.values()) {
|
|
142
|
+
const patched = applyPatches(part, items, next);
|
|
143
|
+
set.set(part.name, encodePart(patched.text));
|
|
144
|
+
}
|
|
145
|
+
// 2. Replaced and added parts; XML parts must scan.
|
|
146
|
+
for (const entry of this.#set.values()) {
|
|
147
|
+
if (/\.(xml|rels)$/i.test(entry.name)) {
|
|
148
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(entry.bytes);
|
|
149
|
+
scanXml(entry.name, text, next);
|
|
150
|
+
}
|
|
151
|
+
set.set(entry.name, entry.bytes);
|
|
152
|
+
}
|
|
153
|
+
// 3. Removed parts take their own .rels part along.
|
|
154
|
+
for (const key of this.#removed) {
|
|
155
|
+
const name = this.#pkg.currentPartNames.find((candidate) => partKey(candidate) === key) ?? key;
|
|
156
|
+
if (!this.#pkg.has(name))
|
|
157
|
+
continue;
|
|
158
|
+
remove.add(name);
|
|
159
|
+
const rels = relationshipsPartOf(name);
|
|
160
|
+
if (this.#pkg.has(rels))
|
|
161
|
+
remove.add(rels);
|
|
162
|
+
}
|
|
163
|
+
// 4. Relationship parts, as patches or new parts.
|
|
164
|
+
const relationshipSources = new Set([
|
|
165
|
+
...this.#relationshipAdds.keys(),
|
|
166
|
+
...this.#relationshipRemoves.keys(),
|
|
167
|
+
]);
|
|
168
|
+
for (const source of relationshipSources) {
|
|
169
|
+
const adds = this.#relationshipAdds.get(source) ?? [];
|
|
170
|
+
const removes = this.#relationshipRemoves.get(source) ?? new Set();
|
|
171
|
+
if (adds.length === 0 && removes.size === 0)
|
|
172
|
+
continue;
|
|
173
|
+
const relsName = relationshipsPartOf(source);
|
|
174
|
+
const existing = await this.#relationships(source);
|
|
175
|
+
const fragments = adds.map((add) => `<Relationship Id=${escapeAttribute(add.id)} Type=${escapeAttribute(add.type)} Target=${escapeAttribute(add.target)}${add.mode === "External" ? ' TargetMode="External"' : ""}/>`);
|
|
176
|
+
if (existing.partName &&
|
|
177
|
+
this.#pkg.has(existing.partName) &&
|
|
178
|
+
!remove.has(existing.partName)) {
|
|
179
|
+
const part = await this.#pkg.xml(existing.partName, signal);
|
|
180
|
+
const items = [];
|
|
181
|
+
for (const id of removes) {
|
|
182
|
+
const node = existing.byId(id)?.node;
|
|
183
|
+
if (!node)
|
|
184
|
+
throw new ViewerError("invalid-patch", `No relationship ${id} in ${existing.partName}`, {
|
|
185
|
+
details: { part: existing.partName, id },
|
|
186
|
+
});
|
|
187
|
+
items.push(patches.removeElement(part, node));
|
|
188
|
+
}
|
|
189
|
+
// One patch per element: each must read back as one element.
|
|
190
|
+
for (const fragment of fragments)
|
|
191
|
+
items.push(patches.appendChild(part, part.root, fragment));
|
|
192
|
+
const patched = applyPatches(part, items, next);
|
|
193
|
+
set.set(existing.partName, encodePart(patched.text));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (removes.size > 0)
|
|
197
|
+
throw new ViewerError("invalid-patch", `No relationships part for ${source}`, { details: { part: relsName } });
|
|
198
|
+
set.set(relsName, encodePart(`${XML_HEADER}<Relationships xmlns="${RELATIONSHIPS_NAMESPACE}">${fragments.join("")}</Relationships>`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 5. Content types: Overrides for new parts with a type, Defaults for media, removals.
|
|
202
|
+
const types = await this.#contentTypes();
|
|
203
|
+
const typesPart = await this.#pkg.xml(CONTENT_TYPES, signal);
|
|
204
|
+
const typeItems = [];
|
|
205
|
+
const additions = [];
|
|
206
|
+
const seenDefaults = new Set();
|
|
207
|
+
for (const entry of this.#set.values()) {
|
|
208
|
+
if (!entry.contentType)
|
|
209
|
+
continue;
|
|
210
|
+
const extension = extensionOf(entry.name);
|
|
211
|
+
const isMedia = !/\.(xml|rels)$/i.test(entry.name) && extension !== "";
|
|
212
|
+
if (isMedia &&
|
|
213
|
+
!types.defaults.has(extension) &&
|
|
214
|
+
!seenDefaults.has(extension)) {
|
|
215
|
+
seenDefaults.add(extension);
|
|
216
|
+
additions.push(`<Default Extension=${escapeAttribute(extension)} ContentType=${escapeAttribute(entry.contentType)}/>`);
|
|
217
|
+
}
|
|
218
|
+
else if (!isMedia &&
|
|
219
|
+
types.defaults.get(extension) !== entry.contentType) {
|
|
220
|
+
const node = types.overrideNode(entry.name);
|
|
221
|
+
if (node)
|
|
222
|
+
typeItems.push(patches.setAttribute(typesPart, node, "ContentType", entry.contentType));
|
|
223
|
+
else
|
|
224
|
+
additions.push(`<Override PartName=${escapeAttribute(entry.name)} ContentType=${escapeAttribute(entry.contentType)}/>`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const name of remove) {
|
|
228
|
+
const node = types.overrideNode(name);
|
|
229
|
+
if (node)
|
|
230
|
+
typeItems.push(patches.removeElement(typesPart, node));
|
|
231
|
+
}
|
|
232
|
+
for (const addition of additions)
|
|
233
|
+
typeItems.push(patches.appendChild(typesPart, typesPart.root, addition));
|
|
234
|
+
if (typeItems.length > 0) {
|
|
235
|
+
const patched = applyPatches(typesPart, typeItems, next);
|
|
236
|
+
set.set(CONTENT_TYPES, encodePart(patched.text));
|
|
237
|
+
}
|
|
238
|
+
// 6. Dangling relationship targets after the removals, as warnings.
|
|
239
|
+
const warnings = [];
|
|
240
|
+
if (remove.size > 0) {
|
|
241
|
+
const removedKeys = new Set([...remove].map(partKey));
|
|
242
|
+
for (const name of this.#pkg.currentPartNames) {
|
|
243
|
+
const source = sourceOfRelationshipsPart(name);
|
|
244
|
+
if (source === undefined || remove.has(name))
|
|
245
|
+
continue;
|
|
246
|
+
const relsPart = set.has(name)
|
|
247
|
+
? scanXml(name, new TextDecoder().decode(set.get(name)), next)
|
|
248
|
+
: await this.#pkg.xml(name, signal);
|
|
249
|
+
const rels = parseRelationships(source, name, relsPart);
|
|
250
|
+
for (const item of rels.items)
|
|
251
|
+
if (item.targetPart && removedKeys.has(partKey(item.targetPart)))
|
|
252
|
+
warnings.push({
|
|
253
|
+
code: "fidelity-degraded",
|
|
254
|
+
message: `Relationship ${item.id} of ${source} points at the removed part ${item.targetPart}`,
|
|
255
|
+
details: {
|
|
256
|
+
source,
|
|
257
|
+
id: item.id,
|
|
258
|
+
target: item.targetPart,
|
|
259
|
+
reason: "dangling-relationship",
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (signal?.aborted)
|
|
265
|
+
throw new ViewerError("aborted", "The transaction was aborted");
|
|
266
|
+
const added = [...set.keys()].filter((name) => !this.#pkg.has(name));
|
|
267
|
+
const changed = [...set.keys()].filter((name) => this.#pkg.has(name));
|
|
268
|
+
this.#pkg.applyOverlay({ set, remove });
|
|
269
|
+
this.#committed = true;
|
|
270
|
+
return {
|
|
271
|
+
changedParts: changed,
|
|
272
|
+
addedParts: added,
|
|
273
|
+
removedParts: [...remove],
|
|
274
|
+
warnings,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
#open() {
|
|
278
|
+
if (this.#committed)
|
|
279
|
+
throw new ViewerError("lifecycle-error", "The transaction was already committed");
|
|
280
|
+
}
|
|
281
|
+
#relationships(source) {
|
|
282
|
+
let pending = this.#relationshipSets.get(source);
|
|
283
|
+
if (!pending) {
|
|
284
|
+
pending = (async () => {
|
|
285
|
+
const relsName = relationshipsPartOf(source);
|
|
286
|
+
if (!this.#pkg.has(relsName) || this.#removed.has(partKey(relsName)))
|
|
287
|
+
return parseRelationships(source, undefined, undefined);
|
|
288
|
+
return parseRelationships(source, relsName, await this.#pkg.xml(relsName));
|
|
289
|
+
})();
|
|
290
|
+
this.#relationshipSets.set(source, pending);
|
|
291
|
+
}
|
|
292
|
+
return pending;
|
|
293
|
+
}
|
|
294
|
+
async #contentTypes() {
|
|
295
|
+
return parseContentTypes(await this.#pkg.xml(CONTENT_TYPES));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** "../media/image1.png" for a target seen from a source part. */
|
|
299
|
+
export function relativeTarget(sourcePart, targetPart) {
|
|
300
|
+
if (sourcePart === "/")
|
|
301
|
+
return partNameOf(targetPart).slice(1);
|
|
302
|
+
const from = partNameOf(sourcePart).split("/").slice(1, -1);
|
|
303
|
+
const to = partNameOf(targetPart).split("/").slice(1);
|
|
304
|
+
let common = 0;
|
|
305
|
+
while (common < from.length &&
|
|
306
|
+
common < to.length - 1 &&
|
|
307
|
+
from[common].toLowerCase() === to[common].toLowerCase())
|
|
308
|
+
common += 1;
|
|
309
|
+
const up = from.length - common;
|
|
310
|
+
return [...Array.from({ length: up }, () => ".."), ...to.slice(common)].join("/");
|
|
311
|
+
}
|
|
312
|
+
async function sha256(bytes) {
|
|
313
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes.slice());
|
|
314
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
315
|
+
}
|
|
316
|
+
export { CONTENT_TYPES_NAMESPACE };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WorkerLike } from "../../worker-client.js";
|
|
2
|
+
import type { EditEngineContext } from "../engine.js";
|
|
3
|
+
export interface OoxmlEditProviderOptions {
|
|
4
|
+
readonly workerUrl?: string | URL;
|
|
5
|
+
/** Test hook: supplies the worker instead of the packaged script. */
|
|
6
|
+
readonly createWorker?: () => WorkerLike;
|
|
7
|
+
}
|
|
8
|
+
export declare function createOoxmlEditWorker(options: OoxmlEditProviderOptions, context: EditEngineContext): WorkerLike;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ViewerError } from "../../errors.js";
|
|
2
|
+
import { packageRelativeUrl, resolveAssetUrl } from "../worker-engine.js";
|
|
3
|
+
export function createOoxmlEditWorker(options, context) {
|
|
4
|
+
if (options.createWorker)
|
|
5
|
+
return options.createWorker();
|
|
6
|
+
if (typeof Worker === "undefined")
|
|
7
|
+
throw new ViewerError("edit-unsupported", `${context.format.toUpperCase()} editing requires a browser module Worker`, { details: { format: context.format, reason: "no-worker" } });
|
|
8
|
+
const url = resolveAssetUrl(options.workerUrl, context.assetBaseUrl, context.assetBaseUrl
|
|
9
|
+
? new URL("workers/ooxml-edit-worker.js", context.assetBaseUrl)
|
|
10
|
+
: packageRelativeUrl("../../../workers/ooxml-edit-worker.js", import.meta.url));
|
|
11
|
+
return new Worker(url, { type: "module", name: "web-doc-ooxml-edit" });
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ZipArchive } from "./zip.js";
|
|
2
|
+
export interface PartChange {
|
|
3
|
+
/** Absolute part name. */
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
export interface WriteOverlay {
|
|
8
|
+
/** Replaced parts by absolute name. */
|
|
9
|
+
readonly changed: ReadonlyMap<string, PartChange>;
|
|
10
|
+
/** New parts by absolute name, in insertion order. */
|
|
11
|
+
readonly added: ReadonlyMap<string, PartChange>;
|
|
12
|
+
/** Removed parts by absolute name. */
|
|
13
|
+
readonly removed: ReadonlySet<string>;
|
|
14
|
+
}
|
|
15
|
+
export interface WriteOptions {
|
|
16
|
+
/** How changed and new entries are written; stored keeps output identical across engines. */
|
|
17
|
+
readonly compression?: "store" | "deflate";
|
|
18
|
+
readonly signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
/** The package bytes for the archive with the overlay applied. */
|
|
21
|
+
export declare function writeZip(archive: ZipArchive, overlay: WriteOverlay, options?: WriteOptions): Promise<Uint8Array>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ViewerError } from "../../errors.js";
|
|
2
|
+
import { entryNameOf, partKey } from "./names.js";
|
|
3
|
+
import { crc32, localRecordOf, METHOD_DEFLATED, METHOD_STORED, } from "./zip.js";
|
|
4
|
+
const SIGNATURE_LOCAL = 0x04034b50;
|
|
5
|
+
const SIGNATURE_CENTRAL = 0x02014b50;
|
|
6
|
+
const SIGNATURE_EOCD = 0x06054b50;
|
|
7
|
+
const VERSION_NEEDED = 20;
|
|
8
|
+
const VERSION_MADE_BY = 0x0314; // UNIX, 2.0
|
|
9
|
+
/** 1980-01-01 00:00 in MS-DOS time and date. */
|
|
10
|
+
const FIXED_TIME = 0;
|
|
11
|
+
const FIXED_DATE = 0x0021;
|
|
12
|
+
const MAX_32 = 0xffffffff;
|
|
13
|
+
const MAX_ENTRIES = 0xffff;
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
/** The package bytes for the archive with the overlay applied. */
|
|
16
|
+
export async function writeZip(archive, overlay, options = {}) {
|
|
17
|
+
const compression = options.compression ?? "store";
|
|
18
|
+
const changed = keyed(overlay.changed);
|
|
19
|
+
const added = keyed(overlay.added);
|
|
20
|
+
const removed = new Set([...overlay.removed].map(partKey));
|
|
21
|
+
const locals = [];
|
|
22
|
+
const centrals = [];
|
|
23
|
+
let offset = 0;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const entry of archive.entries) {
|
|
26
|
+
throwIfAborted(options.signal);
|
|
27
|
+
const key = partKey(entry.name);
|
|
28
|
+
seen.add(key);
|
|
29
|
+
if (removed.has(key))
|
|
30
|
+
continue;
|
|
31
|
+
const change = changed.get(key);
|
|
32
|
+
if (change) {
|
|
33
|
+
const written = await fresh(entry.name, change.bytes, compression, entry);
|
|
34
|
+
locals.push([written.local, written.data]);
|
|
35
|
+
centrals.push(written.central(offset));
|
|
36
|
+
offset += written.local.byteLength + written.data.byteLength;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const local = localRecordOf(archive, entry);
|
|
40
|
+
const record = archive.bytes.subarray(local.headerOffset, local.recordEnd);
|
|
41
|
+
locals.push([record]);
|
|
42
|
+
centrals.push(centralWithOffset(archive, entry, offset));
|
|
43
|
+
offset += record.byteLength;
|
|
44
|
+
}
|
|
45
|
+
for (const [key, change] of added) {
|
|
46
|
+
if (seen.has(key) && !removed.has(key))
|
|
47
|
+
throw new ViewerError("invalid-file", `Part ${key} is added but exists`);
|
|
48
|
+
throwIfAborted(options.signal);
|
|
49
|
+
const name = entryNameOf(change.name);
|
|
50
|
+
const written = await fresh(name, change.bytes, compression);
|
|
51
|
+
locals.push([written.local, written.data]);
|
|
52
|
+
centrals.push(written.central(offset));
|
|
53
|
+
offset += written.local.byteLength + written.data.byteLength;
|
|
54
|
+
}
|
|
55
|
+
if (centrals.length > MAX_ENTRIES)
|
|
56
|
+
throw new ViewerError("unsupported-package", "The package would need ZIP64: more than 65,535 entries");
|
|
57
|
+
const centralOffset = offset;
|
|
58
|
+
let centralLength = 0;
|
|
59
|
+
for (const central of centrals)
|
|
60
|
+
centralLength += central.byteLength;
|
|
61
|
+
if (centralOffset + centralLength > MAX_32)
|
|
62
|
+
throw new ViewerError("unsupported-package", "The package would need ZIP64: it exceeds 4 GiB");
|
|
63
|
+
const eocd = new Uint8Array(22 + archive.comment.byteLength);
|
|
64
|
+
const view = new DataView(eocd.buffer);
|
|
65
|
+
view.setUint32(0, SIGNATURE_EOCD, true);
|
|
66
|
+
view.setUint16(4, 0, true);
|
|
67
|
+
view.setUint16(6, 0, true);
|
|
68
|
+
view.setUint16(8, centrals.length, true);
|
|
69
|
+
view.setUint16(10, centrals.length, true);
|
|
70
|
+
view.setUint32(12, centralLength, true);
|
|
71
|
+
view.setUint32(16, centralOffset, true);
|
|
72
|
+
view.setUint16(20, archive.comment.byteLength, true);
|
|
73
|
+
eocd.set(archive.comment, 22);
|
|
74
|
+
const out = new Uint8Array(centralOffset + centralLength + eocd.byteLength);
|
|
75
|
+
let at = 0;
|
|
76
|
+
for (const parts of locals)
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
out.set(part, at);
|
|
79
|
+
at += part.byteLength;
|
|
80
|
+
}
|
|
81
|
+
for (const central of centrals) {
|
|
82
|
+
out.set(central, at);
|
|
83
|
+
at += central.byteLength;
|
|
84
|
+
}
|
|
85
|
+
out.set(eocd, at);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
function keyed(parts) {
|
|
89
|
+
const map = new Map();
|
|
90
|
+
for (const change of parts.values())
|
|
91
|
+
map.set(partKey(change.name), change);
|
|
92
|
+
return map;
|
|
93
|
+
}
|
|
94
|
+
/** The central record of an untouched entry with the new local-header offset. */
|
|
95
|
+
function centralWithOffset(archive, entry, localHeaderOffset) {
|
|
96
|
+
const record = archive.bytes.slice(entry.centralOffset, entry.centralOffset + entry.centralLength);
|
|
97
|
+
new DataView(record.buffer).setUint32(42, localHeaderOffset, true);
|
|
98
|
+
return record;
|
|
99
|
+
}
|
|
100
|
+
/** Fresh local and central records around stored or deflated data. */
|
|
101
|
+
async function fresh(name, bytes, compression, previous) {
|
|
102
|
+
if (bytes.byteLength > MAX_32)
|
|
103
|
+
throw new ViewerError("unsupported-package", `Part ${name} would need ZIP64: it exceeds 4 GiB`);
|
|
104
|
+
const nameBytes = previous?.nameBytes ?? encoder.encode(name);
|
|
105
|
+
const extra = previous?.extra ?? new Uint8Array(0);
|
|
106
|
+
const comment = previous?.comment ?? new Uint8Array(0);
|
|
107
|
+
const method = compression === "deflate" ? METHOD_DEFLATED : METHOD_STORED;
|
|
108
|
+
const data = method === METHOD_DEFLATED ? await deflateRaw(bytes) : bytes;
|
|
109
|
+
const crc = crc32(bytes);
|
|
110
|
+
const modTime = previous?.modTime ?? FIXED_TIME;
|
|
111
|
+
const modDate = previous?.modDate ?? FIXED_DATE;
|
|
112
|
+
const local = new Uint8Array(30 + nameBytes.byteLength + extra.byteLength);
|
|
113
|
+
const localView = new DataView(local.buffer);
|
|
114
|
+
localView.setUint32(0, SIGNATURE_LOCAL, true);
|
|
115
|
+
localView.setUint16(4, VERSION_NEEDED, true);
|
|
116
|
+
localView.setUint16(6, 0, true);
|
|
117
|
+
localView.setUint16(8, method, true);
|
|
118
|
+
localView.setUint16(10, modTime, true);
|
|
119
|
+
localView.setUint16(12, modDate, true);
|
|
120
|
+
localView.setUint32(14, crc, true);
|
|
121
|
+
localView.setUint32(18, data.byteLength, true);
|
|
122
|
+
localView.setUint32(22, bytes.byteLength, true);
|
|
123
|
+
localView.setUint16(26, nameBytes.byteLength, true);
|
|
124
|
+
localView.setUint16(28, extra.byteLength, true);
|
|
125
|
+
local.set(nameBytes, 30);
|
|
126
|
+
local.set(extra, 30 + nameBytes.byteLength);
|
|
127
|
+
return {
|
|
128
|
+
local,
|
|
129
|
+
data,
|
|
130
|
+
central(localHeaderOffset) {
|
|
131
|
+
const central = new Uint8Array(46 + nameBytes.byteLength + extra.byteLength + comment.byteLength);
|
|
132
|
+
const view = new DataView(central.buffer);
|
|
133
|
+
view.setUint32(0, SIGNATURE_CENTRAL, true);
|
|
134
|
+
view.setUint16(4, previous?.versionMadeBy ?? VERSION_MADE_BY, true);
|
|
135
|
+
view.setUint16(6, VERSION_NEEDED, true);
|
|
136
|
+
view.setUint16(8, 0, true);
|
|
137
|
+
view.setUint16(10, method, true);
|
|
138
|
+
view.setUint16(12, modTime, true);
|
|
139
|
+
view.setUint16(14, modDate, true);
|
|
140
|
+
view.setUint32(16, crc, true);
|
|
141
|
+
view.setUint32(20, data.byteLength, true);
|
|
142
|
+
view.setUint32(24, bytes.byteLength, true);
|
|
143
|
+
view.setUint16(28, nameBytes.byteLength, true);
|
|
144
|
+
view.setUint16(30, extra.byteLength, true);
|
|
145
|
+
view.setUint16(32, comment.byteLength, true);
|
|
146
|
+
view.setUint16(34, 0, true);
|
|
147
|
+
view.setUint16(36, previous?.internalAttributes ?? 0, true);
|
|
148
|
+
view.setUint32(38, previous?.externalAttributes ?? 0, true);
|
|
149
|
+
view.setUint32(42, localHeaderOffset, true);
|
|
150
|
+
central.set(nameBytes, 46);
|
|
151
|
+
central.set(extra, 46 + nameBytes.byteLength);
|
|
152
|
+
central.set(comment, 46 + nameBytes.byteLength + extra.byteLength);
|
|
153
|
+
return central;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function deflateRaw(bytes) {
|
|
158
|
+
if (typeof CompressionStream !== "function")
|
|
159
|
+
throw new ViewerError("edit-unsupported", 'This environment has no CompressionStream; save with compression: "store"', { details: { reason: "no-compression-stream" } });
|
|
160
|
+
const stream = new CompressionStream("deflate-raw");
|
|
161
|
+
const writer = stream.writable.getWriter();
|
|
162
|
+
const writing = writer
|
|
163
|
+
.write(bytes.slice())
|
|
164
|
+
.then(() => writer.close());
|
|
165
|
+
const chunks = [];
|
|
166
|
+
let total = 0;
|
|
167
|
+
const reader = stream.readable.getReader();
|
|
168
|
+
for (;;) {
|
|
169
|
+
const chunk = await reader.read();
|
|
170
|
+
if (chunk.done)
|
|
171
|
+
break;
|
|
172
|
+
chunks.push(chunk.value);
|
|
173
|
+
total += chunk.value.byteLength;
|
|
174
|
+
}
|
|
175
|
+
await writing;
|
|
176
|
+
const out = new Uint8Array(total);
|
|
177
|
+
let at = 0;
|
|
178
|
+
for (const chunk of chunks) {
|
|
179
|
+
out.set(chunk, at);
|
|
180
|
+
at += chunk.byteLength;
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
function throwIfAborted(signal) {
|
|
185
|
+
if (signal?.aborted)
|
|
186
|
+
throw new ViewerError("aborted", "Writing the package was aborted");
|
|
187
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface XmlRange {
|
|
2
|
+
readonly kind: "declaration" | "comment" | "pi" | "cdata" | "doctype";
|
|
3
|
+
readonly start: number;
|
|
4
|
+
readonly end: number;
|
|
5
|
+
}
|
|
6
|
+
export interface XmlAttribute {
|
|
7
|
+
/** Qualified, as written. */
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** Entities decoded. */
|
|
10
|
+
readonly value: string;
|
|
11
|
+
/** As written, without the quotes. */
|
|
12
|
+
readonly rawValue: string;
|
|
13
|
+
/** Of the attribute name. */
|
|
14
|
+
readonly start: number;
|
|
15
|
+
/** After the closing quote. */
|
|
16
|
+
readonly end: number;
|
|
17
|
+
}
|
|
18
|
+
export interface XmlElement {
|
|
19
|
+
/** Qualified, as written: "p:sp". */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly prefix: string;
|
|
22
|
+
readonly local: string;
|
|
23
|
+
/** Resolved through xmlns declarations in scope, when declared. */
|
|
24
|
+
readonly namespace?: string;
|
|
25
|
+
readonly attributes: readonly XmlAttribute[];
|
|
26
|
+
/** "<" of the start tag. */
|
|
27
|
+
readonly start: number;
|
|
28
|
+
/** After ">" of the end tag, or after "/>". */
|
|
29
|
+
readonly end: number;
|
|
30
|
+
/** The content between the tags; empty and equal for a self-closing element. */
|
|
31
|
+
readonly contentStart: number;
|
|
32
|
+
readonly contentEnd: number;
|
|
33
|
+
readonly selfClosing: boolean;
|
|
34
|
+
readonly children: readonly XmlElement[];
|
|
35
|
+
readonly parent?: XmlElement;
|
|
36
|
+
/** Index among the parent's element children. */
|
|
37
|
+
readonly index: number;
|
|
38
|
+
/** Child-index path from the root. */
|
|
39
|
+
readonly path: readonly number[];
|
|
40
|
+
}
|
|
41
|
+
export interface XmlPart {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/** The decoded text the ranges index into (UTF-16 code units). */
|
|
44
|
+
readonly text: string;
|
|
45
|
+
readonly encoding: "utf-8";
|
|
46
|
+
readonly hasBom: boolean;
|
|
47
|
+
/** The overlay revision the part was scanned at; a patch from another revision is stale. */
|
|
48
|
+
readonly revision: number;
|
|
49
|
+
readonly declaration: XmlRange | undefined;
|
|
50
|
+
readonly root: XmlElement;
|
|
51
|
+
/** Comments, processing instructions, CDATA sections and a DOCTYPE, in document order. */
|
|
52
|
+
readonly opaque: readonly XmlRange[];
|
|
53
|
+
find(name: string, under?: XmlElement): XmlElement | undefined;
|
|
54
|
+
findAll(name: string, under?: XmlElement): readonly XmlElement[];
|
|
55
|
+
at(path: readonly number[]): XmlElement | undefined;
|
|
56
|
+
/** Concatenated character data of a subtree, entities decoded, CDATA included. */
|
|
57
|
+
textOf(node: XmlElement): string;
|
|
58
|
+
attribute(node: XmlElement, name: string): string | undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Decodes a part as UTF-8; a part that is not UTF-8 cannot be patched. */
|
|
61
|
+
export declare function decodePart(name: string, bytes: Uint8Array): {
|
|
62
|
+
readonly text: string;
|
|
63
|
+
readonly hasBom: boolean;
|
|
64
|
+
};
|
|
65
|
+
/** The bytes of a part's text; `decodePart` guarantees they equal the original. */
|
|
66
|
+
export declare function encodePart(text: string): Uint8Array;
|
|
67
|
+
/** Scans a part's text; throws `malformed-xml` for what it cannot read. */
|
|
68
|
+
export declare function scanXml(name: string, text: string, revision?: number): XmlPart;
|
|
69
|
+
/** Escapes character data for element content. */
|
|
70
|
+
export declare function escapeText(value: string): string;
|
|
71
|
+
/** Escapes an attribute value and quotes it with double quotes. */
|
|
72
|
+
export declare function escapeAttribute(value: string): string;
|
|
73
|
+
/** Decodes the predefined and numeric entities; anything else stays as written. */
|
|
74
|
+
export declare function decodeEntities(raw: string): string;
|