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.
Files changed (188) hide show
  1. package/THIRD_PARTY_NOTICES.md +26 -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 +47 -2
  14. package/dist/edit/assets.d.ts +23 -0
  15. package/dist/edit/assets.js +75 -0
  16. package/dist/edit/docx/elements.d.ts +6 -0
  17. package/dist/edit/docx/elements.js +90 -0
  18. package/dist/edit/docx/engine.d.ts +51 -0
  19. package/dist/edit/docx/engine.js +473 -0
  20. package/dist/edit/docx/handlers.d.ts +3 -0
  21. package/dist/edit/docx/handlers.js +15 -0
  22. package/dist/edit/docx/ids.d.ts +45 -0
  23. package/dist/edit/docx/ids.js +101 -0
  24. package/dist/edit/docx/model.d.ts +92 -0
  25. package/dist/edit/docx/model.js +339 -0
  26. package/dist/edit/docx/operations.d.ts +37 -0
  27. package/dist/edit/docx/operations.js +3 -0
  28. package/dist/edit/docx/provider.d.ts +13 -0
  29. package/dist/edit/docx/provider.js +34 -0
  30. package/dist/edit/docx/schemas.d.ts +6 -0
  31. package/dist/edit/docx/schemas.js +192 -0
  32. package/dist/edit/docx/session.d.ts +37 -0
  33. package/dist/edit/docx/session.js +426 -0
  34. package/dist/edit/docx/structure-ops.d.ts +6 -0
  35. package/dist/edit/docx/structure-ops.js +494 -0
  36. package/dist/edit/docx/style.d.ts +45 -0
  37. package/dist/edit/docx/style.js +375 -0
  38. package/dist/edit/docx/table-ops.d.ts +4 -0
  39. package/dist/edit/docx/table-ops.js +219 -0
  40. package/dist/edit/docx/text-ops.d.ts +18 -0
  41. package/dist/edit/docx/text-ops.js +458 -0
  42. package/dist/edit/docx/text.d.ts +34 -0
  43. package/dist/edit/docx/text.js +263 -0
  44. package/dist/edit/docx/types.d.ts +191 -0
  45. package/dist/edit/docx/types.js +1 -0
  46. package/dist/edit/docx/write.d.ts +50 -0
  47. package/dist/edit/docx/write.js +352 -0
  48. package/dist/edit/engine.d.ts +117 -0
  49. package/dist/edit/engine.js +1 -0
  50. package/dist/edit/history.d.ts +53 -0
  51. package/dist/edit/history.js +101 -0
  52. package/dist/edit/ooxml/names.d.ts +20 -0
  53. package/dist/edit/ooxml/names.js +61 -0
  54. package/dist/edit/ooxml/opc.d.ts +50 -0
  55. package/dist/edit/ooxml/opc.js +150 -0
  56. package/dist/edit/ooxml/package.d.ts +82 -0
  57. package/dist/edit/ooxml/package.js +233 -0
  58. package/dist/edit/ooxml/patch.d.ts +51 -0
  59. package/dist/edit/ooxml/patch.js +250 -0
  60. package/dist/edit/ooxml/transaction.d.ts +39 -0
  61. package/dist/edit/ooxml/transaction.js +316 -0
  62. package/dist/edit/ooxml/worker.d.ts +8 -0
  63. package/dist/edit/ooxml/worker.js +12 -0
  64. package/dist/edit/ooxml/writer.d.ts +21 -0
  65. package/dist/edit/ooxml/writer.js +187 -0
  66. package/dist/edit/ooxml/xml.d.ts +74 -0
  67. package/dist/edit/ooxml/xml.js +451 -0
  68. package/dist/edit/ooxml/zip.d.ts +54 -0
  69. package/dist/edit/ooxml/zip.js +280 -0
  70. package/dist/edit/operations.d.ts +19 -0
  71. package/dist/edit/operations.js +137 -0
  72. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  73. package/dist/edit/pdf/engine/compact.js +442 -0
  74. package/dist/edit/pdf/engine/document.d.ts +95 -0
  75. package/dist/edit/pdf/engine/document.js +868 -0
  76. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  77. package/dist/edit/pdf/engine/elements.js +313 -0
  78. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  79. package/dist/edit/pdf/engine/existing-text.js +424 -0
  80. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  81. package/dist/edit/pdf/engine/fonts.js +466 -0
  82. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  83. package/dist/edit/pdf/engine/geometry.js +97 -0
  84. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  85. package/dist/edit/pdf/engine/handler.js +108 -0
  86. package/dist/edit/pdf/engine/images.d.ts +33 -0
  87. package/dist/edit/pdf/engine/images.js +188 -0
  88. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  89. package/dist/edit/pdf/engine/layout.js +177 -0
  90. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  91. package/dist/edit/pdf/engine/operations.js +3 -0
  92. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  93. package/dist/edit/pdf/engine/pages.js +98 -0
  94. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  95. package/dist/edit/pdf/engine/pdfium.js +249 -0
  96. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  97. package/dist/edit/pdf/engine/shapes.js +183 -0
  98. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  99. package/dist/edit/pdf/engine/tables.js +315 -0
  100. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  101. package/dist/edit/pdf/engine/text-box.js +317 -0
  102. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  103. package/dist/edit/pdf/engine/text-layout.js +67 -0
  104. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  105. package/dist/edit/pdf/engine/transform.js +137 -0
  106. package/dist/edit/pdf/provider.d.ts +35 -0
  107. package/dist/edit/pdf/provider.js +92 -0
  108. package/dist/edit/pdf/range-map.d.ts +14 -0
  109. package/dist/edit/pdf/range-map.js +107 -0
  110. package/dist/edit/pdf/schemas.d.ts +11 -0
  111. package/dist/edit/pdf/schemas.js +291 -0
  112. package/dist/edit/pdf/selection.d.ts +5 -0
  113. package/dist/edit/pdf/selection.js +152 -0
  114. package/dist/edit/pdf/session.d.ts +53 -0
  115. package/dist/edit/pdf/session.js +273 -0
  116. package/dist/edit/pdf/types.d.ts +336 -0
  117. package/dist/edit/pdf/types.js +1 -0
  118. package/dist/edit/pptx/elements.d.ts +74 -0
  119. package/dist/edit/pptx/elements.js +301 -0
  120. package/dist/edit/pptx/engine.d.ts +37 -0
  121. package/dist/edit/pptx/engine.js +466 -0
  122. package/dist/edit/pptx/geometry.d.ts +72 -0
  123. package/dist/edit/pptx/geometry.js +193 -0
  124. package/dist/edit/pptx/handler.d.ts +7 -0
  125. package/dist/edit/pptx/handler.js +91 -0
  126. package/dist/edit/pptx/handlers.d.ts +3 -0
  127. package/dist/edit/pptx/handlers.js +21 -0
  128. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  129. package/dist/edit/pptx/image-table-ops.js +233 -0
  130. package/dist/edit/pptx/model.d.ts +69 -0
  131. package/dist/edit/pptx/model.js +170 -0
  132. package/dist/edit/pptx/operations.d.ts +50 -0
  133. package/dist/edit/pptx/operations.js +11 -0
  134. package/dist/edit/pptx/provider.d.ts +17 -0
  135. package/dist/edit/pptx/provider.js +40 -0
  136. package/dist/edit/pptx/schemas.d.ts +6 -0
  137. package/dist/edit/pptx/schemas.js +220 -0
  138. package/dist/edit/pptx/session.d.ts +44 -0
  139. package/dist/edit/pptx/session.js +114 -0
  140. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  141. package/dist/edit/pptx/shape-ops.js +486 -0
  142. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  143. package/dist/edit/pptx/slide-ops.js +353 -0
  144. package/dist/edit/pptx/style.d.ts +31 -0
  145. package/dist/edit/pptx/style.js +181 -0
  146. package/dist/edit/pptx/text-ops.d.ts +13 -0
  147. package/dist/edit/pptx/text-ops.js +267 -0
  148. package/dist/edit/pptx/text-write.d.ts +67 -0
  149. package/dist/edit/pptx/text-write.js +293 -0
  150. package/dist/edit/pptx/text.d.ts +41 -0
  151. package/dist/edit/pptx/text.js +99 -0
  152. package/dist/edit/pptx/types.d.ts +232 -0
  153. package/dist/edit/pptx/types.js +1 -0
  154. package/dist/edit/schema.d.ts +11 -0
  155. package/dist/edit/schema.js +275 -0
  156. package/dist/edit/session.d.ts +66 -0
  157. package/dist/edit/session.js +701 -0
  158. package/dist/edit/sessions.d.ts +8 -0
  159. package/dist/edit/sessions.js +1 -0
  160. package/dist/edit/types.d.ts +276 -0
  161. package/dist/edit/types.js +1 -0
  162. package/dist/edit/worker-engine.d.ts +31 -0
  163. package/dist/edit/worker-engine.js +91 -0
  164. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  165. package/dist/fonts/manifest.json +6 -1
  166. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  167. package/dist/fonts.d.ts +2 -0
  168. package/dist/fonts.js +4 -0
  169. package/dist/headless.d.ts +4 -0
  170. package/dist/headless.js +4 -0
  171. package/dist/index.d.ts +5 -0
  172. package/dist/index.js +5 -0
  173. package/dist/limits.js +3 -0
  174. package/dist/ooxml-edit-worker.d.ts +1 -0
  175. package/dist/ooxml-edit-worker.js +8 -0
  176. package/dist/pdf-edit-worker.d.ts +1 -0
  177. package/dist/pdf-edit-worker.js +35 -0
  178. package/dist/spreadsheet-viewport.d.ts +5 -0
  179. package/dist/spreadsheet-viewport.js +11 -0
  180. package/dist/ui.js +7 -0
  181. package/dist/viewer.d.ts +6 -0
  182. package/dist/viewer.js +293 -19
  183. package/dist/viewport.d.ts +13 -0
  184. package/dist/viewport.js +126 -1
  185. package/dist/worker-protocol.d.ts +39 -2
  186. package/dist/workers/ooxml-edit-worker.js +8671 -0
  187. package/dist/workers/pdf-edit-worker.js +10844 -0
  188. package/package.json +3 -3
@@ -0,0 +1,61 @@
1
+ /*
2
+ * OPC part names (ECMA-376 Part 2 §9.1): absolute, "/"-separated, compared
3
+ * case-insensitively. ZIP entry names carry them without the leading slash.
4
+ */
5
+ /** "/ppt/slides/slide1.xml" for a ZIP entry name or a loosely written name. */
6
+ export function partNameOf(entryName) {
7
+ const name = entryName.replaceAll("\\", "/");
8
+ return name.startsWith("/") ? name : `/${name}`;
9
+ }
10
+ /** The ZIP entry name of a part: no leading slash. */
11
+ export function entryNameOf(partName) {
12
+ return partNameOf(partName).slice(1);
13
+ }
14
+ /** The key two part names are compared by. */
15
+ export function partKey(name) {
16
+ return partNameOf(name).toLowerCase();
17
+ }
18
+ /** Whether a ZIP entry is a directory marker rather than a part. */
19
+ export function isDirectoryEntry(entryName) {
20
+ return entryName.endsWith("/");
21
+ }
22
+ /** "/ppt/slides/" for "/ppt/slides/slide1.xml"; "/" at the root. */
23
+ export function folderOf(partName) {
24
+ const name = partNameOf(partName);
25
+ return name.slice(0, name.lastIndexOf("/") + 1);
26
+ }
27
+ /** The extension without the dot, lower-case; "" when there is none. */
28
+ export function extensionOf(partName) {
29
+ const name = partNameOf(partName);
30
+ const slash = name.lastIndexOf("/");
31
+ const dot = name.lastIndexOf(".");
32
+ return dot > slash ? name.slice(dot + 1).toLowerCase() : "";
33
+ }
34
+ /** The relationships part of a part ("/" for the package): "/ppt/slides/_rels/slide1.xml.rels". */
35
+ export function relationshipsPartOf(partName) {
36
+ if (partName === "/" || partName === "")
37
+ return "/_rels/.rels";
38
+ const name = partNameOf(partName);
39
+ const slash = name.lastIndexOf("/");
40
+ return `${name.slice(0, slash + 1)}_rels/${name.slice(slash + 1)}.rels`;
41
+ }
42
+ /**
43
+ * Resolves a relationship target against its source part: relative targets
44
+ * against the source's folder, absolute ones as they are; "." and ".."
45
+ * segments collapse. The result is an absolute part name.
46
+ */
47
+ export function resolveTarget(sourcePart, target) {
48
+ const base = sourcePart === "/" || sourcePart === "" ? "/" : folderOf(sourcePart);
49
+ const raw = target.replaceAll("\\", "/");
50
+ const combined = raw.startsWith("/") ? raw : `${base}${raw}`;
51
+ const segments = [];
52
+ for (const segment of combined.split("/")) {
53
+ if (segment === "" || segment === ".")
54
+ continue;
55
+ if (segment === "..")
56
+ segments.pop();
57
+ else
58
+ segments.push(segment);
59
+ }
60
+ return `/${segments.join("/")}`;
61
+ }
@@ -0,0 +1,50 @@
1
+ import type { XmlElement, XmlPart } from "./xml.js";
2
+ export declare const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
3
+ export declare const RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
4
+ export declare const RELATIONSHIPS_CONTENT_TYPE = "application/vnd.openxmlformats-package.relationships+xml";
5
+ export declare const IMAGE_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
6
+ export interface Relationship {
7
+ readonly id: string;
8
+ readonly type: string;
9
+ /** As written. */
10
+ readonly target: string;
11
+ readonly targetMode: "Internal" | "External";
12
+ /** Absolute part name for internal targets. */
13
+ readonly targetPart?: string;
14
+ /** The element in the scanned .rels part, for patches. */
15
+ readonly node?: XmlElement;
16
+ }
17
+ export declare class RelationshipSet {
18
+ #private;
19
+ /** "/" for the package. */
20
+ readonly sourcePart: string;
21
+ /** The .rels part, when it exists. */
22
+ readonly partName: string | undefined;
23
+ readonly items: readonly Relationship[];
24
+ constructor(
25
+ /** "/" for the package. */
26
+ sourcePart: string,
27
+ /** The .rels part, when it exists. */
28
+ partName: string | undefined, items: readonly Relationship[]);
29
+ byId(id: string): Relationship | undefined;
30
+ byType(type: string): readonly Relationship[];
31
+ /** The first free "rId<n>", counting from 1 and skipping `taken`. */
32
+ nextId(taken?: ReadonlySet<string>): string;
33
+ }
34
+ /** Reads a relationships part; `part` undefined means the source has none yet. */
35
+ export declare function parseRelationships(sourcePart: string, partName: string | undefined, part: XmlPart | undefined): RelationshipSet;
36
+ export interface ContentTypes {
37
+ /** Lower-case extension → content type. */
38
+ readonly defaults: ReadonlyMap<string, string>;
39
+ /** Part-name key → content type. */
40
+ readonly overrides: ReadonlyMap<string, string>;
41
+ typeOf(partName: string): string | undefined;
42
+ /** The Override element of a part, for patches. */
43
+ overrideNode(partName: string): XmlElement | undefined;
44
+ defaultNode(extension: string): XmlElement | undefined;
45
+ }
46
+ export declare function parseContentTypes(part: XmlPart): ContentTypes;
47
+ /** The file extension conventionally used for a media type. */
48
+ export declare function extensionForMime(mimeType: string): string;
49
+ /** The relationships part a .rels file belongs to, or undefined for other parts. */
50
+ export declare function sourceOfRelationshipsPart(partName: string): string | undefined;
@@ -0,0 +1,150 @@
1
+ import { extensionOf, partKey, partNameOf, resolveTarget } from "./names.js";
2
+ /*
3
+ * The Open Packaging Conventions parts the layer understands as models:
4
+ * [Content_Types].xml (Default by extension, Override by part name) and
5
+ * relationship parts (ordered, with ids, types and resolved targets). Both
6
+ * are read from scanned parts and written back as patches by transactions,
7
+ * so untouched entries keep their bytes.
8
+ */
9
+ export const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
10
+ export const RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
11
+ export const RELATIONSHIPS_CONTENT_TYPE = "application/vnd.openxmlformats-package.relationships+xml";
12
+ export const IMAGE_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
13
+ export class RelationshipSet {
14
+ sourcePart;
15
+ partName;
16
+ items;
17
+ #byId = new Map();
18
+ #byType = new Map();
19
+ constructor(
20
+ /** "/" for the package. */
21
+ sourcePart,
22
+ /** The .rels part, when it exists. */
23
+ partName, items) {
24
+ this.sourcePart = sourcePart;
25
+ this.partName = partName;
26
+ this.items = items;
27
+ for (const item of items) {
28
+ this.#byId.set(item.id, item);
29
+ const list = this.#byType.get(item.type);
30
+ if (list)
31
+ list.push(item);
32
+ else
33
+ this.#byType.set(item.type, [item]);
34
+ }
35
+ }
36
+ byId(id) {
37
+ return this.#byId.get(id);
38
+ }
39
+ byType(type) {
40
+ return this.#byType.get(type) ?? [];
41
+ }
42
+ /** The first free "rId<n>", counting from 1 and skipping `taken`. */
43
+ nextId(taken = new Set()) {
44
+ for (let n = 1;; n += 1) {
45
+ const id = `rId${n}`;
46
+ if (!this.#byId.has(id) && !taken.has(id))
47
+ return id;
48
+ }
49
+ }
50
+ }
51
+ /** Reads a relationships part; `part` undefined means the source has none yet. */
52
+ export function parseRelationships(sourcePart, partName, part) {
53
+ if (!part)
54
+ return new RelationshipSet(sourcePart, partName, []);
55
+ const items = [];
56
+ for (const node of part.root.children) {
57
+ if (node.local !== "Relationship")
58
+ continue;
59
+ const id = part.attribute(node, "Id");
60
+ const type = part.attribute(node, "Type");
61
+ const target = part.attribute(node, "Target");
62
+ if (id === undefined || type === undefined || target === undefined)
63
+ continue;
64
+ const external = part.attribute(node, "TargetMode") === "External";
65
+ items.push({
66
+ id,
67
+ type,
68
+ target,
69
+ targetMode: external ? "External" : "Internal",
70
+ ...(external ? {} : { targetPart: resolveTarget(sourcePart, target) }),
71
+ node,
72
+ });
73
+ }
74
+ return new RelationshipSet(sourcePart, partName, items);
75
+ }
76
+ export function parseContentTypes(part) {
77
+ const defaults = new Map();
78
+ const overrides = new Map();
79
+ const defaultNodes = new Map();
80
+ const overrideNodes = new Map();
81
+ for (const node of part.root.children) {
82
+ const contentType = part.attribute(node, "ContentType");
83
+ if (contentType === undefined)
84
+ continue;
85
+ if (node.local === "Default") {
86
+ const extension = part.attribute(node, "Extension")?.toLowerCase();
87
+ if (extension === undefined)
88
+ continue;
89
+ defaults.set(extension, contentType);
90
+ defaultNodes.set(extension, node);
91
+ }
92
+ else if (node.local === "Override") {
93
+ const name = part.attribute(node, "PartName");
94
+ if (name === undefined)
95
+ continue;
96
+ overrides.set(partKey(name), contentType);
97
+ overrideNodes.set(partKey(name), node);
98
+ }
99
+ }
100
+ return {
101
+ defaults,
102
+ overrides,
103
+ typeOf(partName) {
104
+ return (overrides.get(partKey(partName)) ?? defaults.get(extensionOf(partName)));
105
+ },
106
+ overrideNode(partName) {
107
+ return overrideNodes.get(partKey(partName));
108
+ },
109
+ defaultNode(extension) {
110
+ return defaultNodes.get(extension.toLowerCase());
111
+ },
112
+ };
113
+ }
114
+ /** The file extension conventionally used for a media type. */
115
+ export function extensionForMime(mimeType) {
116
+ switch (mimeType.toLowerCase()) {
117
+ case "image/png":
118
+ return "png";
119
+ case "image/jpeg":
120
+ case "image/jpg":
121
+ return "jpeg";
122
+ case "image/gif":
123
+ return "gif";
124
+ case "image/bmp":
125
+ return "bmp";
126
+ case "image/tiff":
127
+ return "tiff";
128
+ case "image/svg+xml":
129
+ return "svg";
130
+ case "image/x-emf":
131
+ case "image/emf":
132
+ return "emf";
133
+ case "image/x-wmf":
134
+ case "image/wmf":
135
+ return "wmf";
136
+ default:
137
+ return "bin";
138
+ }
139
+ }
140
+ /** The relationships part a .rels file belongs to, or undefined for other parts. */
141
+ export function sourceOfRelationshipsPart(partName) {
142
+ const name = partNameOf(partName);
143
+ const match = /^(.*\/)_rels\/([^/]*)\.rels$/.exec(name);
144
+ if (!match)
145
+ return undefined;
146
+ const [, folder, base] = match;
147
+ if (folder === "/" && base === "")
148
+ return "/";
149
+ return `${folder}${base}`;
150
+ }
@@ -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;