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.
Files changed (201) hide show
  1. package/THIRD_PARTY_NOTICES.md +33 -1
  2. package/dist/adapters/docx-images.d.ts +13 -4
  3. package/dist/adapters/docx-images.js +3 -0
  4. package/dist/adapters/docx-paragraphs.d.ts +65 -0
  5. package/dist/adapters/docx-paragraphs.js +165 -0
  6. package/dist/adapters/docx-prepass.d.ts +17 -0
  7. package/dist/adapters/docx-prepass.js +230 -0
  8. package/dist/adapters/office.d.ts +31 -5
  9. package/dist/adapters/office.js +60 -29
  10. package/dist/adapters/pdf.d.ts +9 -0
  11. package/dist/adapters/pdf.js +10 -0
  12. package/dist/assets/pdfium/pdfium.wasm +0 -0
  13. package/dist/contracts.d.ts +53 -2
  14. package/dist/edit/ai/outline.d.ts +47 -0
  15. package/dist/edit/ai/outline.js +338 -0
  16. package/dist/edit/ai/targets.d.ts +16 -0
  17. package/dist/edit/ai/targets.js +309 -0
  18. package/dist/edit/ai/tools.d.ts +28 -0
  19. package/dist/edit/ai/tools.js +605 -0
  20. package/dist/edit/ai/types.d.ts +175 -0
  21. package/dist/edit/ai/types.js +1 -0
  22. package/dist/edit/assets.d.ts +23 -0
  23. package/dist/edit/assets.js +75 -0
  24. package/dist/edit/docx/elements.d.ts +6 -0
  25. package/dist/edit/docx/elements.js +90 -0
  26. package/dist/edit/docx/engine.d.ts +57 -0
  27. package/dist/edit/docx/engine.js +547 -0
  28. package/dist/edit/docx/handlers.d.ts +3 -0
  29. package/dist/edit/docx/handlers.js +15 -0
  30. package/dist/edit/docx/ids.d.ts +45 -0
  31. package/dist/edit/docx/ids.js +101 -0
  32. package/dist/edit/docx/model.d.ts +94 -0
  33. package/dist/edit/docx/model.js +350 -0
  34. package/dist/edit/docx/operations.d.ts +47 -0
  35. package/dist/edit/docx/operations.js +3 -0
  36. package/dist/edit/docx/provider.d.ts +16 -0
  37. package/dist/edit/docx/provider.js +37 -0
  38. package/dist/edit/docx/schemas.d.ts +6 -0
  39. package/dist/edit/docx/schemas.js +192 -0
  40. package/dist/edit/docx/session.d.ts +48 -0
  41. package/dist/edit/docx/session.js +467 -0
  42. package/dist/edit/docx/structure-ops.d.ts +6 -0
  43. package/dist/edit/docx/structure-ops.js +529 -0
  44. package/dist/edit/docx/style.d.ts +45 -0
  45. package/dist/edit/docx/style.js +375 -0
  46. package/dist/edit/docx/table-ops.d.ts +4 -0
  47. package/dist/edit/docx/table-ops.js +241 -0
  48. package/dist/edit/docx/text-ops.d.ts +40 -0
  49. package/dist/edit/docx/text-ops.js +491 -0
  50. package/dist/edit/docx/text.d.ts +34 -0
  51. package/dist/edit/docx/text.js +284 -0
  52. package/dist/edit/docx/tracked.d.ts +52 -0
  53. package/dist/edit/docx/tracked.js +347 -0
  54. package/dist/edit/docx/types.d.ts +206 -0
  55. package/dist/edit/docx/types.js +1 -0
  56. package/dist/edit/docx/write.d.ts +64 -0
  57. package/dist/edit/docx/write.js +375 -0
  58. package/dist/edit/engine.d.ts +128 -0
  59. package/dist/edit/engine.js +1 -0
  60. package/dist/edit/history.d.ts +77 -0
  61. package/dist/edit/history.js +123 -0
  62. package/dist/edit/ooxml/names.d.ts +20 -0
  63. package/dist/edit/ooxml/names.js +61 -0
  64. package/dist/edit/ooxml/opc.d.ts +50 -0
  65. package/dist/edit/ooxml/opc.js +150 -0
  66. package/dist/edit/ooxml/package.d.ts +82 -0
  67. package/dist/edit/ooxml/package.js +233 -0
  68. package/dist/edit/ooxml/patch.d.ts +51 -0
  69. package/dist/edit/ooxml/patch.js +250 -0
  70. package/dist/edit/ooxml/transaction.d.ts +39 -0
  71. package/dist/edit/ooxml/transaction.js +316 -0
  72. package/dist/edit/ooxml/worker.d.ts +8 -0
  73. package/dist/edit/ooxml/worker.js +12 -0
  74. package/dist/edit/ooxml/writer.d.ts +21 -0
  75. package/dist/edit/ooxml/writer.js +187 -0
  76. package/dist/edit/ooxml/xml.d.ts +74 -0
  77. package/dist/edit/ooxml/xml.js +451 -0
  78. package/dist/edit/ooxml/zip.d.ts +54 -0
  79. package/dist/edit/ooxml/zip.js +280 -0
  80. package/dist/edit/operations.d.ts +19 -0
  81. package/dist/edit/operations.js +137 -0
  82. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  83. package/dist/edit/pdf/engine/compact.js +442 -0
  84. package/dist/edit/pdf/engine/document.d.ts +95 -0
  85. package/dist/edit/pdf/engine/document.js +868 -0
  86. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  87. package/dist/edit/pdf/engine/elements.js +313 -0
  88. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  89. package/dist/edit/pdf/engine/existing-text.js +424 -0
  90. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  91. package/dist/edit/pdf/engine/fonts.js +466 -0
  92. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  93. package/dist/edit/pdf/engine/geometry.js +97 -0
  94. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  95. package/dist/edit/pdf/engine/handler.js +108 -0
  96. package/dist/edit/pdf/engine/images.d.ts +33 -0
  97. package/dist/edit/pdf/engine/images.js +188 -0
  98. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  99. package/dist/edit/pdf/engine/layout.js +177 -0
  100. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  101. package/dist/edit/pdf/engine/operations.js +3 -0
  102. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  103. package/dist/edit/pdf/engine/pages.js +98 -0
  104. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  105. package/dist/edit/pdf/engine/pdfium.js +249 -0
  106. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  107. package/dist/edit/pdf/engine/shapes.js +183 -0
  108. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  109. package/dist/edit/pdf/engine/tables.js +315 -0
  110. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  111. package/dist/edit/pdf/engine/text-box.js +317 -0
  112. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  113. package/dist/edit/pdf/engine/text-layout.js +67 -0
  114. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  115. package/dist/edit/pdf/engine/transform.js +137 -0
  116. package/dist/edit/pdf/provider.d.ts +35 -0
  117. package/dist/edit/pdf/provider.js +92 -0
  118. package/dist/edit/pdf/range-map.d.ts +16 -0
  119. package/dist/edit/pdf/range-map.js +107 -0
  120. package/dist/edit/pdf/schemas.d.ts +11 -0
  121. package/dist/edit/pdf/schemas.js +291 -0
  122. package/dist/edit/pdf/selection.d.ts +5 -0
  123. package/dist/edit/pdf/selection.js +152 -0
  124. package/dist/edit/pdf/session.d.ts +63 -0
  125. package/dist/edit/pdf/session.js +312 -0
  126. package/dist/edit/pdf/types.d.ts +336 -0
  127. package/dist/edit/pdf/types.js +1 -0
  128. package/dist/edit/pptx/elements.d.ts +74 -0
  129. package/dist/edit/pptx/elements.js +301 -0
  130. package/dist/edit/pptx/engine.d.ts +37 -0
  131. package/dist/edit/pptx/engine.js +466 -0
  132. package/dist/edit/pptx/geometry.d.ts +72 -0
  133. package/dist/edit/pptx/geometry.js +193 -0
  134. package/dist/edit/pptx/handler.d.ts +7 -0
  135. package/dist/edit/pptx/handler.js +99 -0
  136. package/dist/edit/pptx/handlers.d.ts +3 -0
  137. package/dist/edit/pptx/handlers.js +21 -0
  138. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  139. package/dist/edit/pptx/image-table-ops.js +233 -0
  140. package/dist/edit/pptx/model.d.ts +69 -0
  141. package/dist/edit/pptx/model.js +170 -0
  142. package/dist/edit/pptx/operations.d.ts +50 -0
  143. package/dist/edit/pptx/operations.js +11 -0
  144. package/dist/edit/pptx/provider.d.ts +17 -0
  145. package/dist/edit/pptx/provider.js +40 -0
  146. package/dist/edit/pptx/schemas.d.ts +6 -0
  147. package/dist/edit/pptx/schemas.js +220 -0
  148. package/dist/edit/pptx/session.d.ts +54 -0
  149. package/dist/edit/pptx/session.js +145 -0
  150. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  151. package/dist/edit/pptx/shape-ops.js +486 -0
  152. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  153. package/dist/edit/pptx/slide-ops.js +353 -0
  154. package/dist/edit/pptx/style.d.ts +31 -0
  155. package/dist/edit/pptx/style.js +181 -0
  156. package/dist/edit/pptx/text-ops.d.ts +13 -0
  157. package/dist/edit/pptx/text-ops.js +267 -0
  158. package/dist/edit/pptx/text-write.d.ts +67 -0
  159. package/dist/edit/pptx/text-write.js +293 -0
  160. package/dist/edit/pptx/text.d.ts +41 -0
  161. package/dist/edit/pptx/text.js +99 -0
  162. package/dist/edit/pptx/types.d.ts +232 -0
  163. package/dist/edit/pptx/types.js +1 -0
  164. package/dist/edit/schema.d.ts +11 -0
  165. package/dist/edit/schema.js +275 -0
  166. package/dist/edit/session.d.ts +77 -0
  167. package/dist/edit/session.js +943 -0
  168. package/dist/edit/sessions.d.ts +8 -0
  169. package/dist/edit/sessions.js +1 -0
  170. package/dist/edit/types.d.ts +290 -0
  171. package/dist/edit/types.js +1 -0
  172. package/dist/edit/worker-engine.d.ts +31 -0
  173. package/dist/edit/worker-engine.js +91 -0
  174. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  175. package/dist/fonts/manifest.json +6 -1
  176. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  177. package/dist/fonts.d.ts +2 -0
  178. package/dist/fonts.js +4 -0
  179. package/dist/fuzzy-alignment.d.ts +11 -4
  180. package/dist/fuzzy-alignment.js +3 -9
  181. package/dist/headless.d.ts +5 -1
  182. package/dist/headless.js +8 -1
  183. package/dist/index.d.ts +10 -2
  184. package/dist/index.js +16 -2
  185. package/dist/limits.js +6 -0
  186. package/dist/ooxml-edit-worker.d.ts +1 -0
  187. package/dist/ooxml-edit-worker.js +8 -0
  188. package/dist/pdf-edit-worker.d.ts +1 -0
  189. package/dist/pdf-edit-worker.js +35 -0
  190. package/dist/spreadsheet-viewport.d.ts +5 -0
  191. package/dist/spreadsheet-viewport.js +11 -0
  192. package/dist/ui.js +7 -0
  193. package/dist/viewer.d.ts +6 -0
  194. package/dist/viewer.js +293 -19
  195. package/dist/viewport.d.ts +13 -0
  196. package/dist/viewport.js +126 -1
  197. package/dist/worker-protocol.d.ts +39 -2
  198. package/dist/workers/fuzzy-search-worker.js +1 -1
  199. package/dist/workers/ooxml-edit-worker.js +9212 -0
  200. package/dist/workers/pdf-edit-worker.js +10847 -0
  201. package/package.json +3 -3
@@ -0,0 +1,82 @@
1
+ import type { ResourceLimits } from "../../contracts.js";
2
+ import { type XmlPart } from "./xml.js";
3
+ import { type ContentTypes, type RelationshipSet } from "./opc.js";
4
+ import { PackageTransaction } from "./transaction.js";
5
+ import { type ZipArchive, type ZipEntry } from "./zip.js";
6
+ export interface SaveOptions {
7
+ /** How changed and new entries are written; default "store". */
8
+ readonly compression?: "store" | "deflate";
9
+ }
10
+ /** The overlay at a point in time; opaque to callers, O(1) to take and restore. */
11
+ export interface PackageSnapshot {
12
+ readonly revision: number;
13
+ }
14
+ export interface OpenOptions {
15
+ readonly limits: ResourceLimits;
16
+ readonly signal?: AbortSignal;
17
+ }
18
+ export declare const CONTENT_TYPES_PART = "/[Content_Types].xml";
19
+ export declare class OoxmlPackage {
20
+ #private;
21
+ /** The original bytes, never mutated. */
22
+ readonly original: Uint8Array;
23
+ /** Absolute part names in archive order; directory markers are left out. */
24
+ readonly partNames: readonly string[];
25
+ private constructor();
26
+ /** Parses the central directory; parts are inflated on first use. */
27
+ static open(bytes: Uint8Array, options: OpenOptions): Promise<OoxmlPackage>;
28
+ /** The archive behind the package, for the writer. */
29
+ get archive(): ZipArchive;
30
+ get limits(): ResourceLimits;
31
+ /** Whether a part exists in the current state, overlay included. */
32
+ has(name: string): boolean;
33
+ /** Part names in the current state: archive order, then additions. */
34
+ get currentPartNames(): readonly string[];
35
+ /** Names of the parts that differ from the original: changed, added and removed. */
36
+ get changedParts(): readonly string[];
37
+ get revision(): number;
38
+ /** From [Content_Types].xml: an Override, else the Default for the extension. */
39
+ contentTypeOf(name: string): Promise<string | undefined>;
40
+ /** The content-type model of the current state. */
41
+ contentTypes(): Promise<ContentTypes>;
42
+ /** The relationships of a part, or of the package for "/". */
43
+ relationships(name: string, signal?: AbortSignal): Promise<RelationshipSet>;
44
+ /** Resolves a relationship target against its source part to an absolute name. */
45
+ resolve(sourcePart: string, target: string): string;
46
+ transaction(): PackageTransaction;
47
+ /** The ZIP entry behind a part. */
48
+ entry(name: string): ZipEntry | undefined;
49
+ /** The current bytes of a part: the overlay's when changed, else the original's. */
50
+ part(name: string, signal?: AbortSignal): Promise<Uint8Array>;
51
+ /**
52
+ * A part decoded and scanned, cached until the part changes. A part that
53
+ * is not UTF-8 is `unsupported-part`; one that does not parse is
54
+ * `malformed-xml`.
55
+ */
56
+ xml(name: string, signal?: AbortSignal): Promise<XmlPart>;
57
+ /** The original bytes of a part, inflated once and cached; ignores the overlay. */
58
+ originalPart(name: string, signal?: AbortSignal): Promise<Uint8Array>;
59
+ /**
60
+ * Replaces the overlay with one that has `changes` applied: replaced parts
61
+ * (existing names) and added parts (new names) in `set`, and `remove`.
62
+ * Used by transactions; the previous overlay stays reachable through its
63
+ * snapshot.
64
+ */
65
+ applyOverlay(changes: {
66
+ readonly set: ReadonlyMap<string, Uint8Array>;
67
+ readonly remove: ReadonlySet<string>;
68
+ }): PackageSnapshot;
69
+ /** The overlay as it is now, by reference; restore it later in O(1). */
70
+ snapshot(): PackageSnapshot;
71
+ /**
72
+ * Swaps the overlay back. Parts scanned after the snapshot describe a
73
+ * state that is gone (and a later commit could reach the same revision
74
+ * number with other content), so the scan cache is dropped. Other
75
+ * snapshots stay: a caller may restore a later one again.
76
+ */
77
+ restore(snapshot: PackageSnapshot): void;
78
+ /** Forgets a snapshot that will not be restored, so its overlay can be collected. */
79
+ release(snapshot: PackageSnapshot): void;
80
+ /** The package bytes: the original (copied) when nothing changed. */
81
+ save(options?: SaveOptions, signal?: AbortSignal): Promise<Uint8Array>;
82
+ }
@@ -0,0 +1,233 @@
1
+ import { ViewerError } from "../../errors.js";
2
+ import { isDirectoryEntry, partKey, partNameOf } from "./names.js";
3
+ import { writeZip } from "./writer.js";
4
+ import { decodePart, scanXml } from "./xml.js";
5
+ import { parseContentTypes, parseRelationships, } from "./opc.js";
6
+ import { PackageTransaction } from "./transaction.js";
7
+ import { relationshipsPartOf, resolveTarget } from "./names.js";
8
+ import { inflateEntry, parseZip, } from "./zip.js";
9
+ export const CONTENT_TYPES_PART = "/[Content_Types].xml";
10
+ export class OoxmlPackage {
11
+ /** The original bytes, never mutated. */
12
+ original;
13
+ /** Absolute part names in archive order; directory markers are left out. */
14
+ partNames;
15
+ #archive;
16
+ #limits;
17
+ #entries = new Map();
18
+ #parts = new Map();
19
+ #overlay = {
20
+ revision: 0,
21
+ changed: new Map(),
22
+ added: new Map(),
23
+ removed: new Set(),
24
+ };
25
+ #snapshots = new Map();
26
+ /** Scanned parts by key, valid for the overlay revision they were scanned at. */
27
+ #xml = new Map();
28
+ constructor(archive, limits) {
29
+ this.original = archive.bytes;
30
+ this.#archive = archive;
31
+ this.#limits = limits;
32
+ const names = [];
33
+ for (const entry of archive.entries) {
34
+ if (isDirectoryEntry(entry.name))
35
+ continue;
36
+ const name = partNameOf(entry.name);
37
+ const key = partKey(name);
38
+ if (this.#entries.has(key))
39
+ throw new ViewerError("invalid-file", `Duplicate package part ${name}`);
40
+ this.#entries.set(key, entry);
41
+ names.push(name);
42
+ }
43
+ this.partNames = Object.freeze(names);
44
+ if (!this.has(CONTENT_TYPES_PART))
45
+ throw new ViewerError("invalid-file", "The package has no [Content_Types].xml");
46
+ }
47
+ /** Parses the central directory; parts are inflated on first use. */
48
+ static async open(bytes, options) {
49
+ if (options.signal?.aborted)
50
+ throw new ViewerError("aborted", "Opening the package was aborted");
51
+ return new OoxmlPackage(parseZip(bytes, options.limits), options.limits);
52
+ }
53
+ /** The archive behind the package, for the writer. */
54
+ get archive() {
55
+ return this.#archive;
56
+ }
57
+ get limits() {
58
+ return this.#limits;
59
+ }
60
+ /** Whether a part exists in the current state, overlay included. */
61
+ has(name) {
62
+ const key = partKey(name);
63
+ if (this.#overlay.removed.has(key))
64
+ return false;
65
+ return this.#entries.has(key) || this.#overlay.added.has(key);
66
+ }
67
+ /** Part names in the current state: archive order, then additions. */
68
+ get currentPartNames() {
69
+ const names = this.partNames.filter((name) => this.has(name));
70
+ for (const [, change] of this.#overlay.added)
71
+ names.push(change.name);
72
+ return names;
73
+ }
74
+ /** Names of the parts that differ from the original: changed, added and removed. */
75
+ get changedParts() {
76
+ return [
77
+ ...[...this.#overlay.changed.values()].map((change) => change.name),
78
+ ...[...this.#overlay.added.values()].map((change) => change.name),
79
+ ...[...this.#overlay.removed]
80
+ .map((key) => this.#entries.get(key)?.name ?? key)
81
+ .map(partNameOf),
82
+ ];
83
+ }
84
+ get revision() {
85
+ return this.#overlay.revision;
86
+ }
87
+ /** From [Content_Types].xml: an Override, else the Default for the extension. */
88
+ async contentTypeOf(name) {
89
+ return (await this.contentTypes()).typeOf(name);
90
+ }
91
+ /** The content-type model of the current state. */
92
+ async contentTypes() {
93
+ return parseContentTypes(await this.xml(CONTENT_TYPES_PART));
94
+ }
95
+ /** The relationships of a part, or of the package for "/". */
96
+ async relationships(name, signal) {
97
+ const source = name === "/" || name === "" ? "/" : partNameOf(name);
98
+ const relsName = relationshipsPartOf(source);
99
+ if (!this.has(relsName))
100
+ return parseRelationships(source, undefined, undefined);
101
+ return parseRelationships(source, relsName, await this.xml(relsName, signal));
102
+ }
103
+ /** Resolves a relationship target against its source part to an absolute name. */
104
+ resolve(sourcePart, target) {
105
+ return resolveTarget(sourcePart, target);
106
+ }
107
+ transaction() {
108
+ return new PackageTransaction(this);
109
+ }
110
+ /** The ZIP entry behind a part. */
111
+ entry(name) {
112
+ return this.#entries.get(partKey(name));
113
+ }
114
+ /** The current bytes of a part: the overlay's when changed, else the original's. */
115
+ part(name, signal) {
116
+ const key = partKey(name);
117
+ if (this.#overlay.removed.has(key))
118
+ return Promise.reject(this.#missing(name));
119
+ const change = this.#overlay.changed.get(key) ?? this.#overlay.added.get(key);
120
+ if (change)
121
+ return Promise.resolve(change.bytes.slice());
122
+ return this.originalPart(name, signal);
123
+ }
124
+ /**
125
+ * A part decoded and scanned, cached until the part changes. A part that
126
+ * is not UTF-8 is `unsupported-part`; one that does not parse is
127
+ * `malformed-xml`.
128
+ */
129
+ xml(name, signal) {
130
+ const key = partKey(name);
131
+ const revision = this.#overlay.revision;
132
+ const cached = this.#xml.get(key);
133
+ if (cached)
134
+ return cached.then((part) => part.revision === revision
135
+ ? part
136
+ : this.#rescan(name, key, revision, signal));
137
+ return this.#rescan(name, key, revision, signal);
138
+ }
139
+ #rescan(name, key, revision, signal) {
140
+ const pending = this.part(name, signal).then((bytes) => {
141
+ const partName = partNameOf(name);
142
+ const { text } = decodePart(partName, bytes);
143
+ return scanXml(partName, text, revision);
144
+ });
145
+ this.#xml.set(key, pending);
146
+ pending.catch(() => this.#xml.delete(key));
147
+ return pending;
148
+ }
149
+ /** The original bytes of a part, inflated once and cached; ignores the overlay. */
150
+ originalPart(name, signal) {
151
+ const key = partKey(name);
152
+ const entry = this.#entries.get(key);
153
+ if (!entry)
154
+ return Promise.reject(this.#missing(name));
155
+ let pending = this.#parts.get(key);
156
+ if (!pending) {
157
+ pending = inflateEntry(this.#archive, entry, this.#limits, signal);
158
+ this.#parts.set(key, pending);
159
+ pending.catch(() => this.#parts.delete(key));
160
+ }
161
+ return pending.then((bytes) => bytes.slice());
162
+ }
163
+ /**
164
+ * Replaces the overlay with one that has `changes` applied: replaced parts
165
+ * (existing names) and added parts (new names) in `set`, and `remove`.
166
+ * Used by transactions; the previous overlay stays reachable through its
167
+ * snapshot.
168
+ */
169
+ applyOverlay(changes) {
170
+ const changed = new Map(this.#overlay.changed);
171
+ const added = new Map(this.#overlay.added);
172
+ const removed = new Set(this.#overlay.removed);
173
+ for (const name of changes.remove) {
174
+ const key = partKey(name);
175
+ changed.delete(key);
176
+ added.delete(key);
177
+ if (this.#entries.has(key))
178
+ removed.add(key);
179
+ }
180
+ for (const [name, bytes] of changes.set) {
181
+ const key = partKey(name);
182
+ removed.delete(key);
183
+ const change = { name: partNameOf(name), bytes: bytes.slice() };
184
+ if (this.#entries.has(key))
185
+ changed.set(key, change);
186
+ else
187
+ added.set(key, change);
188
+ }
189
+ const revision = this.#overlay.revision + 1;
190
+ this.#overlay = Object.freeze({ revision, changed, added, removed });
191
+ return { revision };
192
+ }
193
+ /** The overlay as it is now, by reference; restore it later in O(1). */
194
+ snapshot() {
195
+ this.#snapshots.set(this.#overlay.revision, this.#overlay);
196
+ return { revision: this.#overlay.revision };
197
+ }
198
+ /**
199
+ * Swaps the overlay back. Parts scanned after the snapshot describe a
200
+ * state that is gone (and a later commit could reach the same revision
201
+ * number with other content), so the scan cache is dropped. Other
202
+ * snapshots stay: a caller may restore a later one again.
203
+ */
204
+ restore(snapshot) {
205
+ const overlay = this.#snapshots.get(snapshot.revision);
206
+ if (!overlay)
207
+ throw new ViewerError("lifecycle-error", "Unknown package snapshot", {
208
+ details: { revision: snapshot.revision },
209
+ });
210
+ this.#overlay = overlay;
211
+ this.#xml.clear();
212
+ }
213
+ /** Forgets a snapshot that will not be restored, so its overlay can be collected. */
214
+ release(snapshot) {
215
+ if (this.#overlay.revision !== snapshot.revision)
216
+ this.#snapshots.delete(snapshot.revision);
217
+ }
218
+ /** The package bytes: the original (copied) when nothing changed. */
219
+ async save(options = {}, signal) {
220
+ const { changed, added, removed } = this.#overlay;
221
+ if (changed.size === 0 && added.size === 0 && removed.size === 0)
222
+ return this.original.slice();
223
+ return writeZip(this.#archive, this.#overlay, {
224
+ ...(options.compression ? { compression: options.compression } : {}),
225
+ ...(signal ? { signal } : {}),
226
+ });
227
+ }
228
+ #missing(name) {
229
+ return new ViewerError("invalid-file", `No part ${partNameOf(name)}`, {
230
+ details: { part: partNameOf(name) },
231
+ });
232
+ }
233
+ }
@@ -0,0 +1,51 @@
1
+ import { escapeAttribute, escapeText, type XmlElement, type XmlPart } from "./xml.js";
2
+ export type PatchExpectation = {
3
+ readonly kind: "element";
4
+ }
5
+ /** The element starting at `at` (in the unpatched text) has this content afterwards. */
6
+ | {
7
+ readonly kind: "content";
8
+ readonly at: number;
9
+ } | {
10
+ readonly kind: "attribute";
11
+ readonly at: number;
12
+ readonly name: string;
13
+ readonly value: string | undefined;
14
+ } | {
15
+ readonly kind: "removed";
16
+ /** Where the parent starts, how many element children it has without the element, and the element's own text. */
17
+ readonly parentAt: number;
18
+ readonly count: number;
19
+ readonly xml: string;
20
+ } | {
21
+ readonly kind: "none";
22
+ };
23
+ export interface XmlPatch {
24
+ /** Half-open range in the part's text; patches of one part must not overlap. */
25
+ readonly start: number;
26
+ readonly end: number;
27
+ readonly text: string;
28
+ readonly expect: PatchExpectation;
29
+ }
30
+ export declare const patches: {
31
+ /** Replaces the content between the tags; a self-closing element is opened. */
32
+ replaceContent(part: XmlPart, node: XmlElement, xml: string): XmlPatch;
33
+ /** Replaces the whole element with one element. */
34
+ replaceElement(part: XmlPart, node: XmlElement, xml: string): XmlPatch;
35
+ /** Removes the element and nothing around it. */
36
+ removeElement(part: XmlPart, node: XmlElement): XmlPatch;
37
+ insertBefore(part: XmlPart, node: XmlElement, xml: string): XmlPatch;
38
+ insertAfter(part: XmlPart, node: XmlElement, xml: string): XmlPatch;
39
+ /** Appends one element at the end of the content; a self-closing element is opened. */
40
+ appendChild(part: XmlPart, node: XmlElement, xml: string): XmlPatch;
41
+ setAttribute(part: XmlPart, node: XmlElement, name: string, value: string): XmlPatch;
42
+ removeAttribute(part: XmlPart, node: XmlElement, name: string): XmlPatch;
43
+ text: typeof escapeText;
44
+ attr: typeof escapeAttribute;
45
+ };
46
+ /**
47
+ * Applies the patches to the part's text, re-scans it and checks every
48
+ * expectation; returns the new part scanned at `revision`. Throws
49
+ * `invalid-patch` and leaves nothing behind when anything is wrong.
50
+ */
51
+ export declare function applyPatches(part: XmlPart, items: readonly XmlPatch[], revision: number): XmlPart;
@@ -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 };