@bendyline/squisq-formats 2.0.0 → 2.0.1

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 (70) hide show
  1. package/dist/{chunk-CX3LH47J.js → chunk-2KPARF2P.js} +3 -3
  2. package/dist/{chunk-CVNVD7VH.js → chunk-ABVI556T.js} +5 -5
  3. package/dist/{chunk-2CSZJEAL.js → chunk-CRAVSMPZ.js} +3 -3
  4. package/dist/{chunk-H7T5DXUU.js → chunk-FE6OJV6O.js} +95 -14
  5. package/dist/chunk-FE6OJV6O.js.map +1 -0
  6. package/dist/{chunk-2EFDWKC6.js → chunk-HTW2M27H.js} +86 -23
  7. package/dist/chunk-HTW2M27H.js.map +1 -0
  8. package/dist/{chunk-ZZ7D4DN6.js → chunk-QRVN6A6E.js} +39 -9
  9. package/dist/chunk-QRVN6A6E.js.map +1 -0
  10. package/dist/{chunk-XTLQXR4U.js → chunk-U32AG3G3.js} +3 -3
  11. package/dist/{chunk-HBSCRS5P.js → chunk-VJJM2SSH.js} +3 -3
  12. package/dist/{chunk-2PRVLAU3.js → chunk-VSYHZECT.js} +55 -17
  13. package/dist/chunk-VSYHZECT.js.map +1 -0
  14. package/dist/{chunk-4HJ6AK2Q.js → chunk-WC7WULGV.js} +44 -13
  15. package/dist/chunk-WC7WULGV.js.map +1 -0
  16. package/dist/{chunk-X4USWOQ2.js → chunk-XKUMNGBW.js} +2 -2
  17. package/dist/container/index.d.ts +10 -4
  18. package/dist/container/index.js +9 -4
  19. package/dist/container/index.js.map +1 -1
  20. package/dist/docx/index.d.ts +2 -2
  21. package/dist/docx/index.js +4 -4
  22. package/dist/{export-Djev9BLU.d.ts → export-D2NkylDT.d.ts} +1 -1
  23. package/dist/{extract-LNOA7DBK.js → extract-H6RXJMHP.js} +5 -5
  24. package/dist/{import-Yivc3kZM.d.ts → import-K8mfc0fz.d.ts} +1 -1
  25. package/dist/index.d.ts +8 -7
  26. package/dist/index.js +14 -12
  27. package/dist/infer/index.d.ts +4 -4
  28. package/dist/infer/index.js +5 -5
  29. package/dist/{layouts-CZ0bdsNL.d.ts → layouts-BHrgZ5FS.d.ts} +1 -1
  30. package/dist/{layouts-7OUUVAGU.js → layouts-QVPK3ZCU.js} +4 -4
  31. package/dist/ooxml/index.d.ts +4 -4
  32. package/dist/ooxml/index.js +4 -4
  33. package/dist/pptx/index.d.ts +5 -5
  34. package/dist/pptx/index.js +5 -5
  35. package/dist/{reader-Gof6OrJX.d.ts → reader-B9L8Ucbj.d.ts} +3 -3
  36. package/dist/registry/index.d.ts +20 -6
  37. package/dist/registry/index.js +6 -4
  38. package/dist/{themeReader-DPw4J5Ss.d.ts → themeReader-DJKErl_j.d.ts} +1 -1
  39. package/dist/xlsx/index.d.ts +4 -4
  40. package/dist/xlsx/index.js +4 -4
  41. package/dist/{zipLimits-bGYMS9cI.d.ts → zipLimits-BOKCB7qk.d.ts} +2 -0
  42. package/package.json +2 -2
  43. package/src/__tests__/convert.test.ts +143 -1
  44. package/src/__tests__/ooxml.test.ts +17 -0
  45. package/src/__tests__/ooxmlCancellation.test.ts +113 -0
  46. package/src/container/index.ts +17 -3
  47. package/src/index.ts +3 -0
  48. package/src/infer/index.ts +35 -4
  49. package/src/ooxml/reader.ts +55 -14
  50. package/src/pptx/layouts.ts +68 -14
  51. package/src/registry/convert.ts +115 -31
  52. package/src/registry/defaultFormats.ts +5 -2
  53. package/src/registry/index.ts +3 -1
  54. package/src/registry/types.ts +18 -1
  55. package/src/shared/boundedZipArchive.ts +49 -8
  56. package/src/shared/zipEntryCount.ts +59 -5
  57. package/src/shared/zipLimits.ts +10 -0
  58. package/dist/chunk-2EFDWKC6.js.map +0 -1
  59. package/dist/chunk-2PRVLAU3.js.map +0 -1
  60. package/dist/chunk-4HJ6AK2Q.js.map +0 -1
  61. package/dist/chunk-H7T5DXUU.js.map +0 -1
  62. package/dist/chunk-ZZ7D4DN6.js.map +0 -1
  63. /package/dist/{chunk-CX3LH47J.js.map → chunk-2KPARF2P.js.map} +0 -0
  64. /package/dist/{chunk-CVNVD7VH.js.map → chunk-ABVI556T.js.map} +0 -0
  65. /package/dist/{chunk-2CSZJEAL.js.map → chunk-CRAVSMPZ.js.map} +0 -0
  66. /package/dist/{chunk-XTLQXR4U.js.map → chunk-U32AG3G3.js.map} +0 -0
  67. /package/dist/{chunk-HBSCRS5P.js.map → chunk-VJJM2SSH.js.map} +0 -0
  68. /package/dist/{chunk-X4USWOQ2.js.map → chunk-XKUMNGBW.js.map} +0 -0
  69. /package/dist/{extract-LNOA7DBK.js.map → extract-H6RXJMHP.js.map} +0 -0
  70. /package/dist/{layouts-7OUUVAGU.js.map → layouts-QVPK3ZCU.js.map} +0 -0
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { inferThemeFromFile } from '../infer/index';
3
+ import { getPartXml, openPackage } from '../ooxml/reader';
4
+ import { inspectPptxLayouts } from '../pptx/layouts';
5
+ import { prepareZipInput } from '../shared/zipEntryCount';
6
+ import { buildThemedPptx } from './pptxInferFixtures';
7
+
8
+ function blockingBlob(onRead: () => void): Blob {
9
+ const blob = {
10
+ size: 128,
11
+ slice: () => blob,
12
+ arrayBuffer: () => {
13
+ onRead();
14
+ return new Promise<ArrayBuffer>(() => undefined);
15
+ },
16
+ };
17
+ return blob as unknown as Blob;
18
+ }
19
+
20
+ describe('OOXML cancellation', () => {
21
+ it('preserves the exact reason while theme inference reads Blob input', async () => {
22
+ let markStarted: (() => void) | undefined;
23
+ const started = new Promise<void>((resolve) => {
24
+ markStarted = resolve;
25
+ });
26
+ const controller = new AbortController();
27
+ const reason = new Error('stop inference');
28
+ const pending = inferThemeFromFile(
29
+ blockingBlob(() => markStarted?.()),
30
+ {
31
+ signal: controller.signal,
32
+ },
33
+ );
34
+
35
+ await started;
36
+ controller.abort(reason);
37
+
38
+ await expect(pending).rejects.toBe(reason);
39
+ });
40
+
41
+ it('preserves the exact reason while layout inspection opens Blob input', async () => {
42
+ let markStarted: (() => void) | undefined;
43
+ const started = new Promise<void>((resolve) => {
44
+ markStarted = resolve;
45
+ });
46
+ const controller = new AbortController();
47
+ const reason = new Error('stop layout inspection');
48
+ const pending = inspectPptxLayouts(
49
+ blockingBlob(() => markStarted?.()),
50
+ {
51
+ signal: controller.signal,
52
+ },
53
+ );
54
+
55
+ await started;
56
+ controller.abort(reason);
57
+
58
+ await expect(pending).rejects.toBe(reason);
59
+ });
60
+
61
+ it('propagates package cancellation through later OOXML part traversal', async () => {
62
+ const controller = new AbortController();
63
+ const pkg = await openPackage(await buildThemedPptx(), { signal: controller.signal });
64
+ const reason = new Error('stop OOXML traversal');
65
+ controller.abort(reason);
66
+
67
+ await expect(getPartXml(pkg, 'ppt/presentation.xml')).rejects.toBe(reason);
68
+ });
69
+
70
+ it('handles a synchronously re-entrant FileReader abort exactly once', async () => {
71
+ const previous = globalThis.FileReader;
72
+ let abortCalls = 0;
73
+ class ReentrantFileReader {
74
+ result: ArrayBuffer | null = null;
75
+ error: DOMException | null = null;
76
+ onload: ((event: ProgressEvent<FileReader>) => void) | null = null;
77
+ onerror: ((event: ProgressEvent<FileReader>) => void) | null = null;
78
+ onabort: ((event: ProgressEvent<FileReader>) => void) | null = null;
79
+ readAsArrayBuffer(): void {
80
+ // Remain pending until the caller aborts.
81
+ }
82
+
83
+ abort(): void {
84
+ abortCalls += 1;
85
+ this.onabort?.({} as ProgressEvent<FileReader>);
86
+ }
87
+ }
88
+ Object.defineProperty(globalThis, 'FileReader', {
89
+ configurable: true,
90
+ writable: true,
91
+ value: ReentrantFileReader,
92
+ });
93
+
94
+ const blobLike = { size: 1, slice: () => blobLike } as unknown as Blob;
95
+ const controller = new AbortController();
96
+ const reason = new Error('stop FileReader');
97
+ try {
98
+ const pending = prepareZipInput(blobLike, controller.signal);
99
+ controller.abort(reason);
100
+ await expect(pending).rejects.toBe(reason);
101
+ expect(abortCalls).toBe(1);
102
+ } finally {
103
+ if (previous === undefined) Reflect.deleteProperty(globalThis, 'FileReader');
104
+ else {
105
+ Object.defineProperty(globalThis, 'FileReader', {
106
+ configurable: true,
107
+ writable: true,
108
+ value: previous,
109
+ });
110
+ }
111
+ }
112
+ });
113
+ });
@@ -24,6 +24,12 @@ import {
24
24
  } from '../shared/zipSafety.js';
25
25
 
26
26
  export type ZipToContainerOptions = ZipSafetyLimits;
27
+ export interface ContainerToZipOptions {
28
+ /** STORE avoids producing archives that violate strict import compression-ratio policies. */
29
+ compression?: 'STORE' | 'DEFLATE';
30
+ /** JSZip DEFLATE level; ignored for STORE. */
31
+ compressionLevel?: number;
32
+ }
27
33
  export { ZipSafetyError };
28
34
  export type { ZipSafetyLimits, ZipSafetyErrorCode, ZipSafetyErrorOptions };
29
35
 
@@ -36,7 +42,10 @@ export type { ZipSafetyLimits, ZipSafetyErrorCode, ZipSafetyErrorOptions };
36
42
  * @param container — The container to serialize
37
43
  * @returns A Blob containing the ZIP archive
38
44
  */
39
- export async function containerToZip(container: ContentContainer): Promise<Blob> {
45
+ export async function containerToZip(
46
+ container: ContentContainer,
47
+ options: ContainerToZipOptions = {},
48
+ ): Promise<Blob> {
40
49
  const zip = new JSZip();
41
50
  const entries = await container.listFiles();
42
51
 
@@ -48,10 +57,15 @@ export async function containerToZip(container: ContentContainer): Promise<Blob>
48
57
  }
49
58
  }
50
59
 
60
+ const compression = options.compression ?? 'DEFLATE';
61
+ const compressionLevel = options.compressionLevel ?? 6;
62
+ if (!Number.isSafeInteger(compressionLevel) || compressionLevel < 1 || compressionLevel > 9) {
63
+ throw new Error('ZIP compression level must be an integer between 1 and 9');
64
+ }
51
65
  return zip.generateAsync({
52
66
  type: 'blob',
53
- compression: 'DEFLATE',
54
- compressionOptions: { level: 6 },
67
+ compression,
68
+ ...(compression === 'DEFLATE' ? { compressionOptions: { level: compressionLevel } } : {}),
55
69
  });
56
70
  }
57
71
 
package/src/index.ts CHANGED
@@ -76,6 +76,7 @@ export type {
76
76
  // Format registry + programmatic convert()
77
77
  export {
78
78
  convert,
79
+ prepareConversion,
79
80
  createRegistry,
80
81
  defaultRegistry,
81
82
  defaultFormats,
@@ -103,4 +104,6 @@ export type {
103
104
  BuiltinFormatOptions,
104
105
  MarkdownFormatOptions,
105
106
  DbkFormatOptions,
107
+ PreparedConversion,
108
+ PreparedExportOptions,
106
109
  } from './registry/index.js';
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { CustomTemplateDefinition, Theme } from '@bendyline/squisq/schemas';
19
- import { openPackage } from '../ooxml/reader.js';
19
+ import { openPackage, throwIfOoxmlAborted } from '../ooxml/reader.js';
20
20
  import type { OoxmlOpenOptions } from '../ooxml/reader.js';
21
21
  import type { OoxmlPackage } from '../ooxml/types.js';
22
22
  import { ConversionError } from '../registry/errors.js';
@@ -53,7 +53,8 @@ export interface InferredFileTheme {
53
53
  warnings: string[];
54
54
  }
55
55
 
56
- async function looksLikePdf(data: ArrayBuffer | Blob): Promise<boolean> {
56
+ async function looksLikePdf(data: ArrayBuffer | Blob, signal?: AbortSignal): Promise<boolean> {
57
+ throwIfOoxmlAborted(signal);
57
58
  // Duck-typed (not instanceof): buffers routinely cross realms in test
58
59
  // environments and web workers. Blobs carry `size`; ArrayBuffers carry
59
60
  // `byteLength`.
@@ -64,16 +65,39 @@ async function looksLikePdf(data: ArrayBuffer | Blob): Promise<boolean> {
64
65
  typeof blob.slice === 'function' &&
65
66
  typeof blob.arrayBuffer === 'function'
66
67
  ) {
67
- head = new Uint8Array(await blob.slice(0, 5).arrayBuffer());
68
+ head = new Uint8Array(await waitForAbortable(blob.slice(0, 5).arrayBuffer(), signal));
68
69
  } else if (typeof (data as ArrayBuffer).byteLength === 'number') {
69
70
  const buf = data as ArrayBuffer;
70
71
  head = new Uint8Array(buf, 0, Math.min(5, buf.byteLength));
71
72
  }
73
+ throwIfOoxmlAborted(signal);
72
74
  if (!head || head.length < 4) return false;
73
75
  // '%PDF'
74
76
  return head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46;
75
77
  }
76
78
 
79
+ function waitForAbortable<T>(work: Promise<T>, signal?: AbortSignal): Promise<T> {
80
+ throwIfOoxmlAborted(signal);
81
+ if (!signal) return work;
82
+ return new Promise<T>((resolve, reject) => {
83
+ let settled = false;
84
+ const finish = (complete: () => void): void => {
85
+ if (settled) return;
86
+ settled = true;
87
+ signal.removeEventListener('abort', handleAbort);
88
+ complete();
89
+ };
90
+ const handleAbort = (): void =>
91
+ finish(() => reject(signal.reason ?? new Error('OOXML operation was cancelled')));
92
+ signal.addEventListener('abort', handleAbort, { once: true });
93
+ if (signal.aborted) handleAbort();
94
+ work.then(
95
+ (value) => finish(() => resolve(value)),
96
+ (error: unknown) => finish(() => reject(error)),
97
+ );
98
+ });
99
+ }
100
+
77
101
  /**
78
102
  * Classify an opened package from its already-parsed content types.
79
103
  * (Deliberately not the registry's byte sniffer — that re-parses the zip
@@ -110,7 +134,8 @@ export async function inferThemeFromFile(
110
134
  data: ArrayBuffer | Blob,
111
135
  options: InferThemeOptions = {},
112
136
  ): Promise<InferredFileTheme> {
113
- if (await looksLikePdf(data)) {
137
+ throwIfOoxmlAborted(options.signal);
138
+ if (await looksLikePdf(data, options.signal)) {
114
139
  throw new ConversionError(
115
140
  'unsupported-input',
116
141
  'PDF theme inference is not supported — PDF files carry no theme color/font tables.',
@@ -122,6 +147,7 @@ export async function inferThemeFromFile(
122
147
  try {
123
148
  pkg = await openPackage(data, options);
124
149
  } catch (err: unknown) {
150
+ if (options.signal?.aborted) throw options.signal.reason ?? err;
125
151
  throw new ConversionError(
126
152
  'invalid-input',
127
153
  'Could not read this file as a Word, PowerPoint, or Excel document.',
@@ -130,6 +156,7 @@ export async function inferThemeFromFile(
130
156
  }
131
157
 
132
158
  const format = options.format ?? sniffOoxmlFormat(pkg);
159
+ throwIfOoxmlAborted(options.signal);
133
160
  if (!format) {
134
161
  throw new ConversionError(
135
162
  'invalid-input',
@@ -138,6 +165,7 @@ export async function inferThemeFromFile(
138
165
  }
139
166
 
140
167
  const extraction = await EXTRACTORS[format](pkg);
168
+ throwIfOoxmlAborted(options.signal);
141
169
  if (!extraction) {
142
170
  throw new ConversionError('invalid-input', 'No theme part found in this file.', { format });
143
171
  }
@@ -145,6 +173,7 @@ export async function inferThemeFromFile(
145
173
  const { theme, warnings: mapWarnings } = compileExtractedTheme(extraction, {
146
174
  nameHint: options.nameHint,
147
175
  });
176
+ throwIfOoxmlAborted(options.signal);
148
177
  const warnings = [...extraction.warnings, ...mapWarnings];
149
178
 
150
179
  let layouts: CustomTemplateDefinition[] | undefined;
@@ -155,6 +184,7 @@ export async function inferThemeFromFile(
155
184
  const { analyzePptxLayouts } = await import('../pptx/layouts.js');
156
185
  const analysis = await analyzePptxLayouts(pkg, {
157
186
  colors: colorHintsFromExtraction(extraction),
187
+ signal: options.signal,
158
188
  });
159
189
  warnings.push(...analysis.warnings);
160
190
  const defs = analysis.layouts
@@ -164,5 +194,6 @@ export async function inferThemeFromFile(
164
194
  }
165
195
  }
166
196
 
197
+ throwIfOoxmlAborted(options.signal);
167
198
  return { theme, extraction, ...(layouts ? { layouts } : {}), warnings };
168
199
  }
@@ -5,8 +5,8 @@
5
5
  * structural metadata: [Content_Types].xml, relationships, and
6
6
  * core properties.
7
7
  *
8
- * Uses bounded JSZip member streams and a platform DOMParser (with an xmldom
9
- * fallback in bare Node/SSR) to parse XML.
8
+ * Uses bounded JSZip member streams and a platform DOMParser (with the
9
+ * package-owned xmldom parser in Node/SSR) to parse XML.
10
10
  */
11
11
 
12
12
  import { DOMParser as XmldomDOMParser } from '@xmldom/xmldom';
@@ -26,21 +26,44 @@ type DomParserLike = new () => {
26
26
 
27
27
  /** Safe read sessions for opaque packages opened by this module. */
28
28
  const packageArchives = new WeakMap<OoxmlPackage, BoundedZipArchive>();
29
+ const packageSignals = new WeakMap<OoxmlPackage, AbortSignal>();
29
30
  const MAX_CONTENT_TYPES_BYTES = 1024 * 1024;
30
31
  const MAX_RELATIONSHIPS_BYTES = 4 * 1024 * 1024;
32
+ const CANCELLATION_CHECKPOINT_INTERVAL = 256;
33
+
34
+ /** Preserve the caller's exact cancellation reason across OOXML traversal. */
35
+ export function throwIfOoxmlAborted(signal?: AbortSignal): void {
36
+ if (!signal?.aborted) return;
37
+ throw signal.reason ?? new Error('OOXML operation was cancelled');
38
+ }
39
+
40
+ async function cancellationCheckpoint(index: number, signal?: AbortSignal): Promise<void> {
41
+ throwIfOoxmlAborted(signal);
42
+ if (index > 0 && index % CANCELLATION_CHECKPOINT_INTERVAL === 0) {
43
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
44
+ throwIfOoxmlAborted(signal);
45
+ }
46
+ }
31
47
 
32
48
  /**
33
- * Parse XML in browsers and bare Node. Browser DOMParser is preferred; the
34
- * pure-JS xmldom implementation is the package-owned fallback for Node/SSR.
49
+ * Parse XML in browsers and Node. Test runners and SSR hosts can install a
50
+ * partial browser DOMParser whose namespace behavior differs from browsers,
51
+ * so Node always uses the package-owned, namespace-aware xmldom parser.
35
52
  */
36
- function parseXml(source: string): Document {
37
- const nativeParser = (globalThis as { DOMParser?: DomParserLike }).DOMParser;
53
+ function parseXml(source: string, signal?: AbortSignal): Document {
54
+ throwIfOoxmlAborted(signal);
55
+ const runtime = globalThis as {
56
+ DOMParser?: DomParserLike;
57
+ process?: { versions?: { node?: string } };
58
+ };
59
+ const nativeParser = runtime.process?.versions?.node ? undefined : runtime.DOMParser;
38
60
  const Parser = nativeParser ?? (XmldomDOMParser as unknown as DomParserLike);
39
61
  const doc = new Parser().parseFromString(source, 'application/xml') as Document;
40
62
  const parserErrors = doc.getElementsByTagName('parsererror');
41
63
  if (parserErrors.length > 0) {
42
64
  throw new Error(`Invalid OOXML XML part: ${parserErrors[0]?.textContent ?? 'parse error'}`);
43
65
  }
66
+ throwIfOoxmlAborted(signal);
44
67
  return doc;
45
68
  }
46
69
 
@@ -61,15 +84,18 @@ export async function openPackage(
61
84
  data: ArrayBuffer | Blob,
62
85
  limits: OoxmlOpenOptions = {},
63
86
  ): Promise<OoxmlPackage> {
87
+ throwIfOoxmlAborted(limits.signal);
64
88
  const archive = await openBoundedZipArchive(data, limits);
65
- const contentTypes = await parseContentTypes(archive);
66
- const rootRelationships = await parseRelationships(archive, '');
89
+ const contentTypes = await parseContentTypes(archive, limits.signal);
90
+ const rootRelationships = await parseRelationships(archive, '', limits.signal);
91
+ throwIfOoxmlAborted(limits.signal);
67
92
 
68
93
  const pkg = {
69
94
  contentTypes,
70
95
  rootRelationships,
71
96
  } as unknown as OoxmlPackage;
72
97
  packageArchives.set(pkg, archive);
98
+ if (limits.signal) packageSignals.set(pkg, limits.signal);
73
99
  return pkg;
74
100
  }
75
101
 
@@ -80,17 +106,21 @@ export async function openPackage(
80
106
  /**
81
107
  * Parse [Content_Types].xml from the archive.
82
108
  */
83
- async function parseContentTypes(archive: BoundedZipArchive): Promise<ContentTypeMap> {
109
+ async function parseContentTypes(
110
+ archive: BoundedZipArchive,
111
+ signal?: AbortSignal,
112
+ ): Promise<ContentTypeMap> {
84
113
  const overrides = new Map<string, string>();
85
114
  const defaults = new Map<string, string>();
86
115
 
87
116
  const text = await archive.readText('[Content_Types].xml', MAX_CONTENT_TYPES_BYTES);
88
117
  if (text === null) return { overrides, defaults };
89
- const doc = parseXml(text);
118
+ const doc = parseXml(text, signal);
90
119
 
91
120
  // Parse <Default Extension="rels" ContentType="..." />
92
121
  const defaultEls = doc.getElementsByTagName('Default');
93
122
  for (let i = 0; i < defaultEls.length; i++) {
123
+ await cancellationCheckpoint(i, signal);
94
124
  const el = defaultEls[i];
95
125
  const ext = el.getAttribute('Extension');
96
126
  const ct = el.getAttribute('ContentType');
@@ -100,6 +130,7 @@ async function parseContentTypes(archive: BoundedZipArchive): Promise<ContentTyp
100
130
  // Parse <Override PartName="/word/document.xml" ContentType="..." />
101
131
  const overrideEls = doc.getElementsByTagName('Override');
102
132
  for (let i = 0; i < overrideEls.length; i++) {
133
+ await cancellationCheckpoint(i, signal);
103
134
  const el = overrideEls[i];
104
135
  const partName = el.getAttribute('PartName');
105
136
  const ct = el.getAttribute('ContentType');
@@ -128,7 +159,7 @@ export async function getPartRelationships(
128
159
  pkg: OoxmlPackage,
129
160
  partPath: string,
130
161
  ): Promise<Relationship[]> {
131
- return parseRelationships(pkg, partPath);
162
+ return parseRelationships(pkg, partPath, packageSignals.get(pkg));
132
163
  }
133
164
 
134
165
  /**
@@ -140,13 +171,14 @@ export async function getPartRelationships(
140
171
  async function parseRelationships(
141
172
  source: OoxmlPackage | BoundedZipArchive,
142
173
  partPath: string,
174
+ signal?: AbortSignal,
143
175
  ): Promise<Relationship[]> {
144
176
  const relsPath = partPath === '' ? '_rels/.rels' : buildRelsPath(partPath);
145
177
 
146
178
  const bytes = await readArchivePart(source, relsPath, MAX_RELATIONSHIPS_BYTES);
147
179
  if (!bytes) return [];
148
180
  const text = new TextDecoder().decode(bytes);
149
- const doc = parseXml(text);
181
+ const doc = parseXml(text, signal);
150
182
  const result: Relationship[] = [];
151
183
 
152
184
  const els = doc.getElementsByTagNameNS(NS_RELATIONSHIPS, 'Relationship');
@@ -154,6 +186,7 @@ async function parseRelationships(
154
186
  const fallbackEls = els.length > 0 ? els : doc.getElementsByTagName('Relationship');
155
187
 
156
188
  for (let i = 0; i < fallbackEls.length; i++) {
189
+ await cancellationCheckpoint(i, signal);
157
190
  const el = fallbackEls[i];
158
191
  const id = el.getAttribute('Id');
159
192
  const type = el.getAttribute('Type');
@@ -200,8 +233,10 @@ function buildRelsPath(partPath: string): string {
200
233
  * @returns Parsed XML Document, or null if the part doesn't exist
201
234
  */
202
235
  export async function getPartXml(pkg: OoxmlPackage, partPath: string): Promise<Document | null> {
236
+ const signal = packageSignals.get(pkg);
237
+ throwIfOoxmlAborted(signal);
203
238
  const bytes = await readPackagePart(pkg, partPath);
204
- return bytes ? parseXml(new TextDecoder().decode(bytes)) : null;
239
+ return bytes ? parseXml(new TextDecoder().decode(bytes), signal) : null;
205
240
  }
206
241
 
207
242
  /**
@@ -215,6 +250,7 @@ export async function getPartBinary(
215
250
  pkg: OoxmlPackage,
216
251
  partPath: string,
217
252
  ): Promise<ArrayBuffer | null> {
253
+ throwIfOoxmlAborted(packageSignals.get(pkg));
218
254
  const bytes = await readPackagePart(pkg, partPath);
219
255
  return bytes ? bytes.slice().buffer : null;
220
256
  }
@@ -234,6 +270,7 @@ async function readPackagePart(
234
270
  path: string,
235
271
  maxBytes?: number,
236
272
  ): Promise<Uint8Array | null> {
273
+ throwIfOoxmlAborted(packageSignals.get(pkg));
237
274
  const archive = packageArchives.get(pkg);
238
275
  if (!archive) {
239
276
  throw new TypeError('Invalid OoxmlPackage: create packages with openPackage().');
@@ -252,6 +289,8 @@ async function readPackagePart(
252
289
  * @returns Parsed core properties (all fields optional)
253
290
  */
254
291
  export async function getCoreProperties(pkg: OoxmlPackage): Promise<CoreProperties> {
292
+ const signal = packageSignals.get(pkg);
293
+ throwIfOoxmlAborted(signal);
255
294
  const doc = await getPartXml(pkg, 'docProps/core.xml');
256
295
  if (!doc) return {};
257
296
 
@@ -263,7 +302,7 @@ export async function getCoreProperties(pkg: OoxmlPackage): Promise<CoreProperti
263
302
  return undefined;
264
303
  }
265
304
 
266
- return {
305
+ const properties = {
267
306
  title: getText(NS_DC, 'title'),
268
307
  subject: getText(NS_DC, 'subject'),
269
308
  creator: getText(NS_DC, 'creator'),
@@ -274,4 +313,6 @@ export async function getCoreProperties(pkg: OoxmlPackage): Promise<CoreProperti
274
313
  created: getText(NS_DCTERMS, 'created'),
275
314
  modified: getText(NS_DCTERMS, 'modified'),
276
315
  };
316
+ throwIfOoxmlAborted(signal);
317
+ return properties;
277
318
  }