web-doc 0.6.2 → 0.7.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 +26 -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 +47 -2
- 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 +51 -0
- package/dist/edit/docx/engine.js +473 -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 +92 -0
- package/dist/edit/docx/model.js +339 -0
- package/dist/edit/docx/operations.d.ts +37 -0
- package/dist/edit/docx/operations.js +3 -0
- package/dist/edit/docx/provider.d.ts +13 -0
- package/dist/edit/docx/provider.js +34 -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 +37 -0
- package/dist/edit/docx/session.js +426 -0
- package/dist/edit/docx/structure-ops.d.ts +6 -0
- package/dist/edit/docx/structure-ops.js +494 -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 +219 -0
- package/dist/edit/docx/text-ops.d.ts +18 -0
- package/dist/edit/docx/text-ops.js +458 -0
- package/dist/edit/docx/text.d.ts +34 -0
- package/dist/edit/docx/text.js +263 -0
- package/dist/edit/docx/types.d.ts +191 -0
- package/dist/edit/docx/types.js +1 -0
- package/dist/edit/docx/write.d.ts +50 -0
- package/dist/edit/docx/write.js +352 -0
- package/dist/edit/engine.d.ts +117 -0
- package/dist/edit/engine.js +1 -0
- package/dist/edit/history.d.ts +53 -0
- package/dist/edit/history.js +101 -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 +14 -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 +53 -0
- package/dist/edit/pdf/session.js +273 -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 +91 -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 +44 -0
- package/dist/edit/pptx/session.js +114 -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 +66 -0
- package/dist/edit/session.js +701 -0
- package/dist/edit/sessions.d.ts +8 -0
- package/dist/edit/sessions.js +1 -0
- package/dist/edit/types.d.ts +276 -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/headless.d.ts +4 -0
- package/dist/headless.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/limits.js +3 -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/ooxml-edit-worker.js +8671 -0
- package/dist/workers/pdf-edit-worker.js +10844 -0
- package/package.json +3 -3
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { ViewerError } from "../../errors.js";
|
|
2
|
+
import { escapeAttribute, escapeText, scanXml, } from "./xml.js";
|
|
3
|
+
export const patches = {
|
|
4
|
+
/** Replaces the content between the tags; a self-closing element is opened. */
|
|
5
|
+
replaceContent(part, node, xml) {
|
|
6
|
+
if (node.selfClosing)
|
|
7
|
+
return {
|
|
8
|
+
start: node.end - 2,
|
|
9
|
+
end: node.end,
|
|
10
|
+
text: `>${xml}</${node.name}>`,
|
|
11
|
+
expect: { kind: "content", at: node.start },
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
start: node.contentStart,
|
|
15
|
+
end: node.contentEnd,
|
|
16
|
+
text: xml,
|
|
17
|
+
expect: { kind: "content", at: node.start },
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
/** Replaces the whole element with one element. */
|
|
21
|
+
replaceElement(part, node, xml) {
|
|
22
|
+
return {
|
|
23
|
+
start: node.start,
|
|
24
|
+
end: node.end,
|
|
25
|
+
text: xml,
|
|
26
|
+
expect: { kind: "element" },
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
/** Removes the element and nothing around it. */
|
|
30
|
+
removeElement(part, node) {
|
|
31
|
+
if (!node.parent)
|
|
32
|
+
throw new ViewerError("invalid-patch", `Part ${part.name}: the root element cannot be removed`, { details: { part: part.name } });
|
|
33
|
+
return {
|
|
34
|
+
start: node.start,
|
|
35
|
+
end: node.end,
|
|
36
|
+
text: "",
|
|
37
|
+
expect: {
|
|
38
|
+
kind: "removed",
|
|
39
|
+
parentAt: node.parent.start,
|
|
40
|
+
count: node.parent.children.length - 1,
|
|
41
|
+
xml: part.text.slice(node.start, node.end),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
insertBefore(part, node, xml) {
|
|
46
|
+
return {
|
|
47
|
+
start: node.start,
|
|
48
|
+
end: node.start,
|
|
49
|
+
text: xml,
|
|
50
|
+
expect: { kind: "element" },
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
insertAfter(part, node, xml) {
|
|
54
|
+
return {
|
|
55
|
+
start: node.end,
|
|
56
|
+
end: node.end,
|
|
57
|
+
text: xml,
|
|
58
|
+
expect: { kind: "element" },
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
/** Appends one element at the end of the content; a self-closing element is opened. */
|
|
62
|
+
appendChild(part, node, xml) {
|
|
63
|
+
if (node.selfClosing)
|
|
64
|
+
return {
|
|
65
|
+
start: node.end - 2,
|
|
66
|
+
end: node.end,
|
|
67
|
+
text: `>${xml}</${node.name}>`,
|
|
68
|
+
expect: { kind: "content", at: node.start },
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
start: node.contentEnd,
|
|
72
|
+
end: node.contentEnd,
|
|
73
|
+
text: xml,
|
|
74
|
+
expect: { kind: "element" },
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
setAttribute(part, node, name, value) {
|
|
78
|
+
const existing = node.attributes.find((attribute) => attribute.name === name);
|
|
79
|
+
const expect = {
|
|
80
|
+
kind: "attribute",
|
|
81
|
+
at: node.start,
|
|
82
|
+
name,
|
|
83
|
+
value,
|
|
84
|
+
};
|
|
85
|
+
if (existing)
|
|
86
|
+
return {
|
|
87
|
+
start: existing.start,
|
|
88
|
+
end: existing.end,
|
|
89
|
+
text: `${name}=${escapeAttribute(value)}`,
|
|
90
|
+
expect,
|
|
91
|
+
};
|
|
92
|
+
const at = startTagClose(part, node);
|
|
93
|
+
return {
|
|
94
|
+
start: at,
|
|
95
|
+
end: at,
|
|
96
|
+
text: ` ${name}=${escapeAttribute(value)}`,
|
|
97
|
+
expect,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
removeAttribute(part, node, name) {
|
|
101
|
+
const existing = node.attributes.find((attribute) => attribute.name === name);
|
|
102
|
+
const expect = {
|
|
103
|
+
kind: "attribute",
|
|
104
|
+
at: node.start,
|
|
105
|
+
name,
|
|
106
|
+
value: undefined,
|
|
107
|
+
};
|
|
108
|
+
if (!existing)
|
|
109
|
+
return { start: node.start, end: node.start, text: "", expect };
|
|
110
|
+
// Take the whitespace before the attribute with it.
|
|
111
|
+
let start = existing.start;
|
|
112
|
+
while (start > node.start && /\s/.test(part.text[start - 1]))
|
|
113
|
+
start -= 1;
|
|
114
|
+
return { start, end: existing.end, text: "", expect };
|
|
115
|
+
},
|
|
116
|
+
text: escapeText,
|
|
117
|
+
attr: escapeAttribute,
|
|
118
|
+
};
|
|
119
|
+
/** Where the start tag's ">" or "/>" begins. */
|
|
120
|
+
function startTagClose(part, node) {
|
|
121
|
+
if (node.selfClosing)
|
|
122
|
+
return node.end - 2;
|
|
123
|
+
const last = node.attributes.at(-1);
|
|
124
|
+
const from = last ? last.end : node.start + 1 + node.name.length;
|
|
125
|
+
const close = part.text.indexOf(">", from);
|
|
126
|
+
if (close < 0 || close >= node.contentStart)
|
|
127
|
+
throw new ViewerError("internal", `Start tag of ${node.name} has no close`);
|
|
128
|
+
return close;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Applies the patches to the part's text, re-scans it and checks every
|
|
132
|
+
* expectation; returns the new part scanned at `revision`. Throws
|
|
133
|
+
* `invalid-patch` and leaves nothing behind when anything is wrong.
|
|
134
|
+
*/
|
|
135
|
+
export function applyPatches(part, items, revision) {
|
|
136
|
+
const sorted = [...items].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
137
|
+
let cursor = 0;
|
|
138
|
+
for (const patch of sorted) {
|
|
139
|
+
if (!Number.isInteger(patch.start) ||
|
|
140
|
+
!Number.isInteger(patch.end) ||
|
|
141
|
+
patch.start < 0 ||
|
|
142
|
+
patch.end < patch.start ||
|
|
143
|
+
patch.end > part.text.length)
|
|
144
|
+
throw invalid(part, patch, "the range is outside the part");
|
|
145
|
+
if (patch.start < cursor)
|
|
146
|
+
throw invalid(part, patch, "the ranges overlap");
|
|
147
|
+
cursor = patch.end;
|
|
148
|
+
}
|
|
149
|
+
// Build the new text from the front, remembering where each patch lands.
|
|
150
|
+
let out = "";
|
|
151
|
+
let from = 0;
|
|
152
|
+
const landed = [];
|
|
153
|
+
for (const patch of sorted) {
|
|
154
|
+
out += part.text.slice(from, patch.start);
|
|
155
|
+
landed.push(out.length);
|
|
156
|
+
out += patch.text;
|
|
157
|
+
from = patch.end;
|
|
158
|
+
}
|
|
159
|
+
out += part.text.slice(from);
|
|
160
|
+
let rescanned;
|
|
161
|
+
try {
|
|
162
|
+
rescanned = scanXml(part.name, out, revision);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw new ViewerError("invalid-patch", `Part ${part.name}: the patched part is not well-formed`, {
|
|
166
|
+
cause: error,
|
|
167
|
+
details: {
|
|
168
|
+
part: part.name,
|
|
169
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const byStart = new Map();
|
|
174
|
+
const stack = [rescanned.root];
|
|
175
|
+
while (stack.length > 0) {
|
|
176
|
+
const node = stack.pop();
|
|
177
|
+
byStart.set(node.start, node);
|
|
178
|
+
for (const child of node.children)
|
|
179
|
+
stack.push(child);
|
|
180
|
+
}
|
|
181
|
+
/** Where an unpatched offset lands: shifted by every patch that ends at or before it. */
|
|
182
|
+
const mapped = (offset) => {
|
|
183
|
+
let delta = 0;
|
|
184
|
+
for (const patch of sorted) {
|
|
185
|
+
if (patch.end > offset)
|
|
186
|
+
break;
|
|
187
|
+
delta += patch.text.length - (patch.end - patch.start);
|
|
188
|
+
}
|
|
189
|
+
return offset + delta;
|
|
190
|
+
};
|
|
191
|
+
sorted.forEach((patch, index) => {
|
|
192
|
+
const at = landed[index];
|
|
193
|
+
const { expect } = patch;
|
|
194
|
+
switch (expect.kind) {
|
|
195
|
+
case "element": {
|
|
196
|
+
const node = byStart.get(at);
|
|
197
|
+
if (!node || node.end !== at + patch.text.length)
|
|
198
|
+
throw invalid(part, patch, "the fragment does not read back as one element", rescanned.text.slice(at, at + patch.text.length));
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "content": {
|
|
202
|
+
const node = byStart.get(mapped(expect.at));
|
|
203
|
+
if (!node)
|
|
204
|
+
throw invalid(part, patch, "the patched element is gone");
|
|
205
|
+
const content = rescanned.text.slice(node.contentStart, node.contentEnd);
|
|
206
|
+
const wanted = node.selfClosing
|
|
207
|
+
? ""
|
|
208
|
+
: patch.text.startsWith(">") && patch.text.endsWith(`</${node.name}>`)
|
|
209
|
+
? patch.text.slice(1, -(node.name.length + 3))
|
|
210
|
+
: patch.text;
|
|
211
|
+
if (content !== wanted)
|
|
212
|
+
throw invalid(part, patch, "the content does not read back", content);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case "attribute": {
|
|
216
|
+
const node = byStart.get(mapped(expect.at));
|
|
217
|
+
if (!node)
|
|
218
|
+
throw invalid(part, patch, "the patched element is gone");
|
|
219
|
+
const read = rescanned.attribute(node, expect.name);
|
|
220
|
+
if (read !== expect.value)
|
|
221
|
+
throw invalid(part, patch, `attribute ${expect.name} reads back differently`, read);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "removed": {
|
|
225
|
+
const parent = byStart.get(mapped(expect.parentAt));
|
|
226
|
+
if (!parent)
|
|
227
|
+
throw invalid(part, patch, "the removed element's parent is gone");
|
|
228
|
+
// Another patch may have put a sibling in the same place; then the
|
|
229
|
+
// removed element's own text must at least be gone from there.
|
|
230
|
+
if (parent.children.length !== expect.count &&
|
|
231
|
+
rescanned.text.startsWith(expect.xml, at))
|
|
232
|
+
throw invalid(part, patch, "the element is still there", String(parent.children.length));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "none":
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return rescanned;
|
|
240
|
+
}
|
|
241
|
+
function invalid(part, patch, reason, read) {
|
|
242
|
+
return new ViewerError("invalid-patch", `Part ${part.name}: ${reason}`, {
|
|
243
|
+
details: {
|
|
244
|
+
part: part.name,
|
|
245
|
+
range: [patch.start, patch.end],
|
|
246
|
+
reason,
|
|
247
|
+
...(read === undefined ? {} : { read: read.slice(0, 200) }),
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ViewerWarning } from "../../contracts.js";
|
|
2
|
+
import { CONTENT_TYPES_NAMESPACE } from "./opc.js";
|
|
3
|
+
import type { OoxmlPackage } from "./package.js";
|
|
4
|
+
import { type XmlPatch } from "./patch.js";
|
|
5
|
+
import { type XmlPart } from "./xml.js";
|
|
6
|
+
export interface CommittedChange {
|
|
7
|
+
readonly changedParts: readonly string[];
|
|
8
|
+
readonly addedParts: readonly string[];
|
|
9
|
+
readonly removedParts: readonly string[];
|
|
10
|
+
/** One per relationship whose internal target no longer exists. */
|
|
11
|
+
readonly warnings: readonly ViewerWarning[];
|
|
12
|
+
}
|
|
13
|
+
export declare class PackageTransaction {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(pkg: OoxmlPackage);
|
|
16
|
+
/** Patches of one scanned part; the scan must be of the current revision. */
|
|
17
|
+
patch(part: XmlPart, items: readonly XmlPatch[]): void;
|
|
18
|
+
/** Adds or replaces a part; `contentType` registers it unless a Default covers the extension. */
|
|
19
|
+
setPart(name: string, bytes: Uint8Array, contentType?: string): void;
|
|
20
|
+
removePart(name: string): void;
|
|
21
|
+
/** Allocates the next free rId of the source's .rels part, pending adds included. */
|
|
22
|
+
addRelationship(sourcePart: string, type: string, target: string, mode?: "Internal" | "External"): Promise<string>;
|
|
23
|
+
removeRelationship(sourcePart: string, id: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Stores media once per distinct content under `folder`, registers the
|
|
26
|
+
* extension's content type when missing, and relates it to `sourcePart`.
|
|
27
|
+
*/
|
|
28
|
+
addMedia(sourcePart: string, folder: string, bytes: Uint8Array, mimeType: string, relationshipType?: string): Promise<{
|
|
29
|
+
readonly part: string;
|
|
30
|
+
readonly rId: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** A part name that does not exist yet, in the package or in this transaction. */
|
|
33
|
+
uniquePartName(prefix: string, extension: string): string;
|
|
34
|
+
/** Applies everything or nothing; returns what changed. */
|
|
35
|
+
commit(signal?: AbortSignal): Promise<CommittedChange>;
|
|
36
|
+
}
|
|
37
|
+
/** "../media/image1.png" for a target seen from a source part. */
|
|
38
|
+
export declare function relativeTarget(sourcePart: string, targetPart: string): string;
|
|
39
|
+
export { CONTENT_TYPES_NAMESPACE };
|
|
@@ -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>;
|