@yolk-sdk/extractors 0.1.0-canary.98

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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/dist/errors.d.mts +60 -0
  4. package/dist/errors.d.mts.map +1 -0
  5. package/dist/errors.mjs +69 -0
  6. package/dist/errors.mjs.map +1 -0
  7. package/dist/format.d.mts +32 -0
  8. package/dist/format.d.mts.map +1 -0
  9. package/dist/format.mjs +52 -0
  10. package/dist/format.mjs.map +1 -0
  11. package/dist/index.d.mts +6 -0
  12. package/dist/index.mjs +6 -0
  13. package/dist/knowledge.d.mts +17 -0
  14. package/dist/knowledge.d.mts.map +1 -0
  15. package/dist/knowledge.mjs +77 -0
  16. package/dist/knowledge.mjs.map +1 -0
  17. package/dist/limits.d.mts +28 -0
  18. package/dist/limits.d.mts.map +1 -0
  19. package/dist/limits.mjs +43 -0
  20. package/dist/limits.mjs.map +1 -0
  21. package/dist/node/extract-file.d.mts +31 -0
  22. package/dist/node/extract-file.d.mts.map +1 -0
  23. package/dist/node/extract-file.mjs +183 -0
  24. package/dist/node/extract-file.mjs.map +1 -0
  25. package/dist/node/extraction-isolation.d.mts +94 -0
  26. package/dist/node/extraction-isolation.d.mts.map +1 -0
  27. package/dist/node/extraction-isolation.mjs +155 -0
  28. package/dist/node/extraction-isolation.mjs.map +1 -0
  29. package/dist/node/extraction-worker-protocol.d.mts +60 -0
  30. package/dist/node/extraction-worker-protocol.d.mts.map +1 -0
  31. package/dist/node/extraction-worker-protocol.mjs +105 -0
  32. package/dist/node/extraction-worker-protocol.mjs.map +1 -0
  33. package/dist/node/extraction-worker.d.mts +1 -0
  34. package/dist/node/extraction-worker.mjs +114729 -0
  35. package/dist/node/index.d.mts +6 -0
  36. package/dist/node/index.mjs +5 -0
  37. package/dist/node/live-layer.d.mts +36 -0
  38. package/dist/node/live-layer.d.mts.map +1 -0
  39. package/dist/node/live-layer.mjs +70 -0
  40. package/dist/node/live-layer.mjs.map +1 -0
  41. package/dist/node/office-archive.d.mts +51 -0
  42. package/dist/node/office-archive.d.mts.map +1 -0
  43. package/dist/node/office-archive.mjs +193 -0
  44. package/dist/node/office-archive.mjs.map +1 -0
  45. package/dist/node/pptx-text.d.mts +6 -0
  46. package/dist/node/pptx-text.d.mts.map +1 -0
  47. package/dist/node/pptx-text.mjs +63 -0
  48. package/dist/node/pptx-text.mjs.map +1 -0
  49. package/dist/node/sheetjs-xml.d.mts +89 -0
  50. package/dist/node/sheetjs-xml.d.mts.map +1 -0
  51. package/dist/node/sheetjs-xml.mjs +253 -0
  52. package/dist/node/sheetjs-xml.mjs.map +1 -0
  53. package/dist/node/sheetjs.d.mts +62 -0
  54. package/dist/node/sheetjs.d.mts.map +1 -0
  55. package/dist/node/sheetjs.mjs +122 -0
  56. package/dist/node/sheetjs.mjs.map +1 -0
  57. package/dist/node/worker-admission.d.mts +58 -0
  58. package/dist/node/worker-admission.d.mts.map +1 -0
  59. package/dist/node/worker-admission.mjs +107 -0
  60. package/dist/node/worker-admission.mjs.map +1 -0
  61. package/dist/node/xlsx-hyperlinks.d.mts +34 -0
  62. package/dist/node/xlsx-hyperlinks.d.mts.map +1 -0
  63. package/dist/node/xlsx-hyperlinks.mjs +159 -0
  64. package/dist/node/xlsx-hyperlinks.mjs.map +1 -0
  65. package/dist/node/xlsx-parts.d.mts +29 -0
  66. package/dist/node/xlsx-parts.d.mts.map +1 -0
  67. package/dist/node/xlsx-parts.mjs +49 -0
  68. package/dist/node/xlsx-parts.mjs.map +1 -0
  69. package/dist/node/xlsx-range.d.mts +21 -0
  70. package/dist/node/xlsx-range.d.mts.map +1 -0
  71. package/dist/node/xlsx-range.mjs +49 -0
  72. package/dist/node/xlsx-range.mjs.map +1 -0
  73. package/dist/node/xlsx-routing.d.mts +36 -0
  74. package/dist/node/xlsx-routing.d.mts.map +1 -0
  75. package/dist/node/xlsx-routing.mjs +115 -0
  76. package/dist/node/xlsx-routing.mjs.map +1 -0
  77. package/dist/node/xlsx-sheetjs-input.d.mts +29 -0
  78. package/dist/node/xlsx-sheetjs-input.d.mts.map +1 -0
  79. package/dist/node/xlsx-sheetjs-input.mjs +165 -0
  80. package/dist/node/xlsx-sheetjs-input.mjs.map +1 -0
  81. package/dist/node/xlsx-styles.d.mts +37 -0
  82. package/dist/node/xlsx-styles.d.mts.map +1 -0
  83. package/dist/node/xlsx-styles.mjs +96 -0
  84. package/dist/node/xlsx-styles.mjs.map +1 -0
  85. package/dist/node/xlsx-text.d.mts +32 -0
  86. package/dist/node/xlsx-text.d.mts.map +1 -0
  87. package/dist/node/xlsx-text.mjs +181 -0
  88. package/dist/node/xlsx-text.mjs.map +1 -0
  89. package/dist/node/xlsx-workbook.d.mts +32 -0
  90. package/dist/node/xlsx-workbook.d.mts.map +1 -0
  91. package/dist/node/xlsx-workbook.mjs +70 -0
  92. package/dist/node/xlsx-workbook.mjs.map +1 -0
  93. package/dist/node/xml-text.d.mts +12 -0
  94. package/dist/node/xml-text.d.mts.map +1 -0
  95. package/dist/node/xml-text.mjs +51 -0
  96. package/dist/node/xml-text.mjs.map +1 -0
  97. package/dist/sanitize.d.mts +6 -0
  98. package/dist/sanitize.d.mts.map +1 -0
  99. package/dist/sanitize.mjs +11 -0
  100. package/dist/sanitize.mjs.map +1 -0
  101. package/dist/service.d.mts +22 -0
  102. package/dist/service.d.mts.map +1 -0
  103. package/dist/service.mjs +11 -0
  104. package/dist/service.mjs.map +1 -0
  105. package/package.json +87 -0
  106. package/src/errors.ts +96 -0
  107. package/src/format.ts +84 -0
  108. package/src/index.ts +32 -0
  109. package/src/knowledge.ts +101 -0
  110. package/src/limits.ts +49 -0
  111. package/src/node/extract-file.ts +269 -0
  112. package/src/node/extraction-isolation.ts +289 -0
  113. package/src/node/extraction-worker-protocol.ts +130 -0
  114. package/src/node/extraction-worker.ts +56 -0
  115. package/src/node/index.ts +21 -0
  116. package/src/node/live-layer.ts +136 -0
  117. package/src/node/office-archive.ts +368 -0
  118. package/src/node/pptx-text.ts +125 -0
  119. package/src/node/sheetjs-xml.ts +356 -0
  120. package/src/node/sheetjs.ts +177 -0
  121. package/src/node/worker-admission.ts +162 -0
  122. package/src/node/xlsx-hyperlinks.ts +260 -0
  123. package/src/node/xlsx-parts.ts +83 -0
  124. package/src/node/xlsx-range.ts +70 -0
  125. package/src/node/xlsx-routing.ts +171 -0
  126. package/src/node/xlsx-sheetjs-input.ts +275 -0
  127. package/src/node/xlsx-styles.ts +160 -0
  128. package/src/node/xlsx-text.ts +288 -0
  129. package/src/node/xlsx-workbook.ts +133 -0
  130. package/src/node/xml-text.ts +77 -0
  131. package/src/sanitize.ts +18 -0
  132. package/src/service.ts +21 -0
@@ -0,0 +1,107 @@
1
+ import { Effect } from "effect";
2
+ import * as Schema from "effect/Schema";
3
+ //#region src/node/worker-admission.ts
4
+ /**
5
+ * Worker admission. Every Node `FileExtractor` layer in a JavaScript realm (the main thread, or
6
+ * each worker thread or `vm` context that builds the layer) shares one pool of
7
+ * `processWorkerLimit` worker slots, however often the layer is built (a per-request
8
+ * `Effect.provide` builds a new one each time). The pool lives on `globalThis` under a versioned
9
+ * `Symbol.for` key and holds only plain data and plain callbacks, so duplicated copies of this
10
+ * package (and of Effect) in one realm share it. Each layer also has its own pool of
11
+ * `maxConcurrentWorkers` slots (at most `processWorkerLimit`), which can only lower that layer's
12
+ * share. An extraction takes its layer's slot, then a realm slot, and waits for both until its
13
+ * admission deadline; a slot is released only after its worker has terminated.
14
+ *
15
+ * Admission is first come, first served: a freed slot is handed to the longest-waiting extraction
16
+ * whose deadline has not passed, and a new extraction takes a free slot only when nobody waits.
17
+ */
18
+ /** Worker slots shared by every layer in the realm. */
19
+ const processWorkerLimit = 4;
20
+ const makeSlotPool = (capacity) => ({
21
+ capacity,
22
+ active: 0,
23
+ waiters: /* @__PURE__ */ new Set()
24
+ });
25
+ /** The version names the hand-off protocol above; a change to it needs a new key. */
26
+ const processPoolKey = Symbol.for("@yolk-sdk/extractors/worker-admission/v2");
27
+ /** Waiters are only ever added by `withSlot`, in this module or a copy of it. */
28
+ const WaiterSet = Schema.declare((value) => value instanceof Set);
29
+ /** The pool's shape, checked when another copy of this module may have created it. */
30
+ const isSlotPool = Schema.is(Schema.Struct({
31
+ capacity: Schema.Number,
32
+ active: Schema.Number,
33
+ waiters: WaiterSet
34
+ }));
35
+ /** The realm-wide pool, created by the first copy of this module that asks for it. */
36
+ const processSlotPool = () => {
37
+ const existing = Object.getOwnPropertyDescriptor(globalThis, processPoolKey)?.value;
38
+ if (isSlotPool(existing)) return existing;
39
+ const pool = makeSlotPool(4);
40
+ Object.defineProperty(globalThis, processPoolKey, {
41
+ value: pool,
42
+ configurable: false
43
+ });
44
+ return pool;
45
+ };
46
+ /** Running and waiting extractions in the realm-wide pool. */
47
+ const processAdmissionSnapshot = () => {
48
+ const pool = processSlotPool();
49
+ return {
50
+ active: pool.active,
51
+ waiting: pool.waiters.size
52
+ };
53
+ };
54
+ /** Hand the slot to the first waiter that can still take it; otherwise it becomes free. */
55
+ const releaseSlot = (pool) => {
56
+ for (const handOff of pool.waiters) if (handOff()) return;
57
+ pool.active -= 1;
58
+ };
59
+ /** Whether `deadline` (epoch ms) has passed. */
60
+ const expired = (deadline) => Date.now() > deadline;
61
+ /**
62
+ * Run `self` holding one slot of `pool`, waiting for it until `deadline` (epoch ms) and failing
63
+ * with `busy()` after that. The deadline is checked again whenever a slot would be taken, by a new
64
+ * extraction or by a hand-off, so a late wake-up cannot turn an expired wait into admission.
65
+ *
66
+ * Taking a slot and registering its release happen without an interruption point between them,
67
+ * so a slot is never leaked; only the wait is interruptible. A slot handed to a waiter whose
68
+ * fiber is interrupted before it resumes is released again. The wait uses a real timer, not the
69
+ * Effect `Clock`, so a test clock cannot stall it.
70
+ */
71
+ const withSlot = (pool, deadline, busy) => (self) => Effect.uninterruptibleMask((restore) => Effect.suspend(() => {
72
+ let owned = false;
73
+ return restore(Effect.callback((resume) => {
74
+ if (expired(deadline)) return resume(Effect.succeed(false));
75
+ if (pool.active < pool.capacity && pool.waiters.size === 0) {
76
+ pool.active += 1;
77
+ owned = true;
78
+ return resume(Effect.succeed(true));
79
+ }
80
+ const handOff = () => {
81
+ stop();
82
+ if (expired(deadline)) {
83
+ queueMicrotask(() => resume(Effect.succeed(false)));
84
+ return false;
85
+ }
86
+ owned = true;
87
+ queueMicrotask(() => resume(Effect.succeed(true)));
88
+ return true;
89
+ };
90
+ const timer = setTimeout(() => {
91
+ stop();
92
+ resume(Effect.succeed(false));
93
+ }, deadline - Date.now());
94
+ const stop = () => {
95
+ clearTimeout(timer);
96
+ pool.waiters.delete(handOff);
97
+ };
98
+ pool.waiters.add(handOff);
99
+ return Effect.sync(stop);
100
+ })).pipe(Effect.onInterrupt(() => Effect.sync(() => {
101
+ if (owned) releaseSlot(pool);
102
+ })), Effect.flatMap((admitted) => admitted ? restore(self).pipe(Effect.ensuring(Effect.sync(() => releaseSlot(pool)))) : Effect.fail(busy())));
103
+ }));
104
+ //#endregion
105
+ export { makeSlotPool, processAdmissionSnapshot, processSlotPool, processWorkerLimit, withSlot };
106
+
107
+ //# sourceMappingURL=worker-admission.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-admission.mjs","names":[],"sources":["../../src/node/worker-admission.ts"],"sourcesContent":["import { Effect } from 'effect'\nimport * as Schema from 'effect/Schema'\n\n/**\n * Worker admission. Every Node `FileExtractor` layer in a JavaScript realm (the main thread, or\n * each worker thread or `vm` context that builds the layer) shares one pool of\n * `processWorkerLimit` worker slots, however often the layer is built (a per-request\n * `Effect.provide` builds a new one each time). The pool lives on `globalThis` under a versioned\n * `Symbol.for` key and holds only plain data and plain callbacks, so duplicated copies of this\n * package (and of Effect) in one realm share it. Each layer also has its own pool of\n * `maxConcurrentWorkers` slots (at most `processWorkerLimit`), which can only lower that layer's\n * share. An extraction takes its layer's slot, then a realm slot, and waits for both until its\n * admission deadline; a slot is released only after its worker has terminated.\n *\n * Admission is first come, first served: a freed slot is handed to the longest-waiting extraction\n * whose deadline has not passed, and a new extraction takes a free slot only when nobody waits.\n */\n\n/** Worker slots shared by every layer in the realm. */\nexport const processWorkerLimit = 4\n\n/**\n * Takes a freed slot for its waiter and returns `true`, or returns `false` when the waiter's\n * deadline has passed (it then fails with `busy`). Either way it leaves the queue. It never runs\n * the waiter's fiber: the resume is deferred to a microtask, so a waiter that finishes at once\n * cannot release (and hand off) again inside this call, and a long queue drains in constant stack.\n */\nexport type SlotHandOff = () => boolean\n\n/**\n * A counting pool of worker slots with a FIFO queue (a `Set` iterates in insertion order). While\n * anyone waits, every slot is taken: a released slot passes to a waiter without being counted\n * free.\n */\nexport type SlotPool = {\n readonly capacity: number\n active: number\n readonly waiters: Set<SlotHandOff>\n}\n\nexport const makeSlotPool = (capacity: number): SlotPool => ({\n capacity,\n active: 0,\n waiters: new Set()\n})\n\n/** The version names the hand-off protocol above; a change to it needs a new key. */\nconst processPoolKey = Symbol.for('@yolk-sdk/extractors/worker-admission/v2')\n\n/** Waiters are only ever added by `withSlot`, in this module or a copy of it. */\nconst WaiterSet = Schema.declare((value): value is Set<SlotHandOff> => value instanceof Set)\n\n/** The pool's shape, checked when another copy of this module may have created it. */\nconst isSlotPool = Schema.is(\n Schema.Struct({ capacity: Schema.Number, active: Schema.Number, waiters: WaiterSet })\n)\n\n/** The realm-wide pool, created by the first copy of this module that asks for it. */\nexport const processSlotPool = (): SlotPool => {\n const existing: unknown = Object.getOwnPropertyDescriptor(globalThis, processPoolKey)?.value\n\n if (isSlotPool(existing)) return existing\n\n const pool = makeSlotPool(processWorkerLimit)\n\n Object.defineProperty(globalThis, processPoolKey, { value: pool, configurable: false })\n\n return pool\n}\n\n/** Running and waiting extractions in the realm-wide pool. */\nexport const processAdmissionSnapshot = () => {\n const pool = processSlotPool()\n\n return { active: pool.active, waiting: pool.waiters.size }\n}\n\n/** Hand the slot to the first waiter that can still take it; otherwise it becomes free. */\nconst releaseSlot = (pool: SlotPool) => {\n for (const handOff of pool.waiters) if (handOff()) return\n\n pool.active -= 1\n}\n\n/** Whether `deadline` (epoch ms) has passed. */\nconst expired = (deadline: number) => Date.now() > deadline\n\n/**\n * Run `self` holding one slot of `pool`, waiting for it until `deadline` (epoch ms) and failing\n * with `busy()` after that. The deadline is checked again whenever a slot would be taken, by a new\n * extraction or by a hand-off, so a late wake-up cannot turn an expired wait into admission.\n *\n * Taking a slot and registering its release happen without an interruption point between them,\n * so a slot is never leaked; only the wait is interruptible. A slot handed to a waiter whose\n * fiber is interrupted before it resumes is released again. The wait uses a real timer, not the\n * Effect `Clock`, so a test clock cannot stall it.\n */\nexport const withSlot =\n <E2>(pool: SlotPool, deadline: number, busy: () => E2) =>\n <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E | E2, R> =>\n Effect.uninterruptibleMask(restore =>\n Effect.suspend(() => {\n // Set once this fiber owns a slot, before its release is registered.\n let owned = false\n\n const admission = Effect.callback<boolean>(resume => {\n if (expired(deadline)) return resume(Effect.succeed(false))\n\n if (pool.active < pool.capacity && pool.waiters.size === 0) {\n pool.active += 1\n owned = true\n\n return resume(Effect.succeed(true))\n }\n\n const handOff: SlotHandOff = () => {\n stop()\n\n if (expired(deadline)) {\n queueMicrotask(() => resume(Effect.succeed(false)))\n\n return false\n }\n\n // The slot is this fiber's from here on. If it is interrupted before the deferred\n // resume runs, Effect ignores that resume and `onInterrupt` passes the slot on.\n owned = true\n queueMicrotask(() => resume(Effect.succeed(true)))\n\n return true\n }\n\n const timer = setTimeout(() => {\n stop()\n resume(Effect.succeed(false))\n }, deadline - Date.now())\n\n const stop = () => {\n clearTimeout(timer)\n pool.waiters.delete(handOff)\n }\n\n pool.waiters.add(handOff)\n\n return Effect.sync(stop)\n })\n\n return restore(admission).pipe(\n // Interrupted after a hand-off but before resuming: pass the slot on.\n Effect.onInterrupt(() =>\n Effect.sync(() => {\n if (owned) releaseSlot(pool)\n })\n ),\n Effect.flatMap((admitted): Effect.Effect<A, E | E2, R> =>\n admitted\n ? restore(self).pipe(Effect.ensuring(Effect.sync(() => releaseSlot(pool))))\n : Effect.fail(busy())\n )\n )\n })\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,MAAa,qBAAqB;AAqBlC,MAAa,gBAAgB,cAAgC;CAC3D;CACA,QAAQ;CACR,yBAAS,IAAI,IAAI;AACnB;;AAGA,MAAM,iBAAiB,OAAO,IAAI,0CAA0C;;AAG5E,MAAM,YAAY,OAAO,SAAS,UAAqC,iBAAiB,GAAG;;AAG3F,MAAM,aAAa,OAAO,GACxB,OAAO,OAAO;CAAE,UAAU,OAAO;CAAQ,QAAQ,OAAO;CAAQ,SAAS;AAAU,CAAC,CACtF;;AAGA,MAAa,wBAAkC;CAC7C,MAAM,WAAoB,OAAO,yBAAyB,YAAY,cAAc,GAAG;CAEvF,IAAI,WAAW,QAAQ,GAAG,OAAO;CAEjC,MAAM,OAAO,aAAA,CAA+B;CAE5C,OAAO,eAAe,YAAY,gBAAgB;EAAE,OAAO;EAAM,cAAc;CAAM,CAAC;CAEtF,OAAO;AACT;;AAGA,MAAa,iCAAiC;CAC5C,MAAM,OAAO,gBAAgB;CAE7B,OAAO;EAAE,QAAQ,KAAK;EAAQ,SAAS,KAAK,QAAQ;CAAK;AAC3D;;AAGA,MAAM,eAAe,SAAmB;CACtC,KAAK,MAAM,WAAW,KAAK,SAAS,IAAI,QAAQ,GAAG;CAEnD,KAAK,UAAU;AACjB;;AAGA,MAAM,WAAW,aAAqB,KAAK,IAAI,IAAI;;;;;;;;;;;AAYnD,MAAa,YACN,MAAgB,UAAkB,UAC7B,SACR,OAAO,qBAAoB,YACzB,OAAO,cAAc;CAEnB,IAAI,QAAQ;CA4CZ,OAAO,QA1CW,OAAO,UAAkB,WAAU;EACnD,IAAI,QAAQ,QAAQ,GAAG,OAAO,OAAO,OAAO,QAAQ,KAAK,CAAC;EAE1D,IAAI,KAAK,SAAS,KAAK,YAAY,KAAK,QAAQ,SAAS,GAAG;GAC1D,KAAK,UAAU;GACf,QAAQ;GAER,OAAO,OAAO,OAAO,QAAQ,IAAI,CAAC;EACpC;EAEA,MAAM,gBAA6B;GACjC,KAAK;GAEL,IAAI,QAAQ,QAAQ,GAAG;IACrB,qBAAqB,OAAO,OAAO,QAAQ,KAAK,CAAC,CAAC;IAElD,OAAO;GACT;GAIA,QAAQ;GACR,qBAAqB,OAAO,OAAO,QAAQ,IAAI,CAAC,CAAC;GAEjD,OAAO;EACT;EAEA,MAAM,QAAQ,iBAAiB;GAC7B,KAAK;GACL,OAAO,OAAO,QAAQ,KAAK,CAAC;EAC9B,GAAG,WAAW,KAAK,IAAI,CAAC;EAExB,MAAM,aAAa;GACjB,aAAa,KAAK;GAClB,KAAK,QAAQ,OAAO,OAAO;EAC7B;EAEA,KAAK,QAAQ,IAAI,OAAO;EAExB,OAAO,OAAO,KAAK,IAAI;CACzB,CAEuB,CAAC,EAAE,KAExB,OAAO,kBACL,OAAO,WAAW;EAChB,IAAI,OAAO,YAAY,IAAI;CAC7B,CAAC,CACH,GACA,OAAO,SAAS,aACd,WACI,QAAQ,IAAI,EAAE,KAAK,OAAO,SAAS,OAAO,WAAW,YAAY,IAAI,CAAC,CAAC,CAAC,IACxE,OAAO,KAAK,KAAK,CAAC,CACxB,CACF;AACF,CAAC,CACH"}
@@ -0,0 +1,34 @@
1
+ import { StrippedHyperlinkTags } from "./office-archive.mjs";
2
+ import { CellRange } from "./xlsx-range.mjs";
3
+
4
+ //#region src/node/xlsx-hyperlinks.d.ts
5
+ type XlsxHyperlink = {
6
+ readonly range: CellRange; /** Normalized `http:`, `https:`, or `mailto:` URL, at most 2,048 characters. */
7
+ readonly target: string; /** Label for cells without text, at most 1,024 characters (longer labels end in `…`). */
8
+ readonly display?: string;
9
+ };
10
+ /** Hyperlinks per worksheet name, in document order (a later link wins on overlap). */
11
+ type XlsxHyperlinks = ReadonlyMap<string, ReadonlyArray<XlsxHyperlink>>;
12
+ /** A sheet of the workbook and the validated worksheet part SheetJS reads for it, if any. */
13
+ type HyperlinkSheet = {
14
+ readonly name: string;
15
+ readonly part: string | undefined;
16
+ };
17
+ /**
18
+ * Map the hyperlink tags removed from a workbook to its sheets (the same sheet list and worksheet
19
+ * parts SheetJS reads, from `buildSheetJsInput`) through each worksheet's relationships.
20
+ */
21
+ declare const resolveXlsxHyperlinks: (parts: Readonly<Record<string, Uint8Array>>, hyperlinkTags: StrippedHyperlinkTags, sheets: ReadonlyArray<HyperlinkSheet>) => XlsxHyperlinks;
22
+ type HyperlinkLookup = {
23
+ /** The winning link at a cell. Rows must be queried in ascending order. */readonly at: (row: number, column: number) => XlsxHyperlink | undefined;
24
+ };
25
+ /**
26
+ * Index links clipped to the visited range for row-major lookup without scanning every link per
27
+ * cell: a segment tree over columns whose nodes hold max-heaps (by document order) of the links
28
+ * covering them. Links enter when the sweep reaches their first row and are discarded lazily once
29
+ * it passes their last row. Work is O((links + cells) · log(columns) · log(links)).
30
+ */
31
+ declare const makeHyperlinkLookup: (links: ReadonlyArray<XlsxHyperlink>, visited: CellRange) => HyperlinkLookup;
32
+ //#endregion
33
+ export { HyperlinkLookup, HyperlinkSheet, XlsxHyperlink, XlsxHyperlinks, makeHyperlinkLookup, resolveXlsxHyperlinks };
34
+ //# sourceMappingURL=xlsx-hyperlinks.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-hyperlinks.d.mts","names":[],"sources":["../../src/node/xlsx-hyperlinks.ts"],"mappings":";;;;KAQY,aAAA;EAAA,SACD,KAAA,EAAO,SAAS,EADf;EAAA,SAGD,MAAA;WAEA,OAAA;AAAA;;KAIC,cAAA,GAAiB,WAAA,SAAoB,aAAA,CAAc,aAAA;;KAkEnD,cAAA;EAAA,SACD,IAAA;EAAA,SACA,IAAI;AAAA;;;;;cAOF,qBAAA,GACX,KAAA,EAAO,QAAA,CAAS,MAAA,SAAe,UAAA,IAC/B,aAAA,EAAe,qBAAA,EACf,MAAA,EAAQ,aAAA,CAAc,cAAA,MACrB,cAAA;AAAA,KAoCS,eAAA;EAnHiB,oFAqHlB,EAAA,GAAK,GAAA,UAAa,MAAA,aAAmB,aAAa;AAAA;;AArHe;AAkE5E;;;;cA4Da,mBAAA,GACX,KAAA,EAAO,aAAA,CAAc,aAAA,GACrB,OAAA,EAAS,SAAA,KACR,eAAA"}
@@ -0,0 +1,159 @@
1
+ import { prefixedAttribute, xmlAttributes } from "./xml-text.mjs";
2
+ import { indexParts, relationships, relationshipsPathFor, utf8Text } from "./xlsx-parts.mjs";
3
+ import { parseCellRange } from "./xlsx-range.mjs";
4
+ import { Buffer } from "node:buffer";
5
+ //#region src/node/xlsx-hyperlinks.ts
6
+ /** Longer targets are dropped: a truncated URL would point somewhere else. */
7
+ const maxHyperlinkTargetCharacters = 2048;
8
+ /** Longer display labels are cut to this many characters, ending with an ellipsis. */
9
+ const maxHyperlinkDisplayCharacters = 1024;
10
+ const capDisplay = (display) => {
11
+ if (display.length <= maxHyperlinkDisplayCharacters) return display;
12
+ let kept = display.slice(0, maxHyperlinkDisplayCharacters - 1);
13
+ const last = kept.charCodeAt(kept.length - 1);
14
+ if (last >= 55296 && last <= 56319) kept = kept.slice(0, -1);
15
+ return `${kept}\u2026`;
16
+ };
17
+ const shownProtocols = new Set([
18
+ "http:",
19
+ "https:",
20
+ "mailto:"
21
+ ]);
22
+ const shownTarget = (target) => {
23
+ if (target.length > maxHyperlinkTargetCharacters || !URL.canParse(target)) return void 0;
24
+ const url = new URL(target);
25
+ if (!shownProtocols.has(url.protocol) || url.href.length > maxHyperlinkTargetCharacters) return void 0;
26
+ return url.href;
27
+ };
28
+ const hyperlinkFrom = (tag, sheetRelationships) => {
29
+ const attributes = xmlAttributes(Buffer.from(tag, "latin1").toString("utf8"));
30
+ const ref = attributes.get("ref");
31
+ const range = ref === void 0 ? void 0 : parseCellRange(ref);
32
+ const id = prefixedAttribute(attributes, "id");
33
+ const relationship = id === void 0 ? void 0 : sheetRelationships.get(id);
34
+ if (range === void 0 || relationship === void 0 || !relationship.external) return void 0;
35
+ const location = attributes.get("location");
36
+ const target = shownTarget(location === void 0 || location.length === 0 ? relationship.target : `${relationship.target}#${location}`);
37
+ if (target === void 0) return void 0;
38
+ const display = attributes.get("display")?.trim();
39
+ return display === void 0 || display.length === 0 ? {
40
+ range,
41
+ target
42
+ } : {
43
+ range,
44
+ target,
45
+ display: capDisplay(display)
46
+ };
47
+ };
48
+ /**
49
+ * Map the hyperlink tags removed from a workbook to its sheets (the same sheet list and worksheet
50
+ * parts SheetJS reads, from `buildSheetJsInput`) through each worksheet's relationships.
51
+ */
52
+ const resolveXlsxHyperlinks = (parts, hyperlinkTags, sheets) => {
53
+ const bySheet = /* @__PURE__ */ new Map();
54
+ if (hyperlinkTags.size === 0) return bySheet;
55
+ const index = indexParts(parts);
56
+ const text = (path) => utf8Text(index.find(path)?.bytes);
57
+ for (const { name, part } of sheets) {
58
+ const tags = part === void 0 ? void 0 : hyperlinkTags.get(part);
59
+ if (part === void 0 || tags === void 0 || bySheet.has(name)) continue;
60
+ const sheetRelationships = relationships(text(relationshipsPathFor(part)));
61
+ const links = tags.flatMap((linkTag) => {
62
+ const link = hyperlinkFrom(linkTag, sheetRelationships);
63
+ return link === void 0 ? [] : [link];
64
+ });
65
+ if (links.length > 0) bySheet.set(name, links);
66
+ }
67
+ return bySheet;
68
+ };
69
+ /**
70
+ * Index links clipped to the visited range for row-major lookup without scanning every link per
71
+ * cell: a segment tree over columns whose nodes hold max-heaps (by document order) of the links
72
+ * covering them. Links enter when the sweep reaches their first row and are discarded lazily once
73
+ * it passes their last row. Work is O((links + cells) · log(columns) · log(links)).
74
+ */
75
+ const makeHyperlinkLookup = (links, visited) => {
76
+ const clipped = links.flatMap((link, order) => {
77
+ const firstRow = Math.max(link.range.start.r, visited.start.r);
78
+ const lastRow = Math.min(link.range.end.r, visited.end.r);
79
+ const firstColumn = Math.max(link.range.start.c, visited.start.c) - visited.start.c;
80
+ const lastColumn = Math.min(link.range.end.c, visited.end.c) - visited.start.c;
81
+ return firstRow > lastRow || firstColumn > lastColumn ? [] : [{
82
+ order,
83
+ firstRow,
84
+ lastRow,
85
+ firstColumn,
86
+ lastColumn,
87
+ link
88
+ }];
89
+ }).sort((left, right) => left.firstRow - right.firstRow || left.order - right.order);
90
+ if (clipped.length === 0) return { at: () => void 0 };
91
+ const width = visited.end.c - visited.start.c + 1;
92
+ let leaves = 1;
93
+ while (leaves < width) leaves *= 2;
94
+ const heaps = [];
95
+ const push = (node, entry) => {
96
+ const heap = heaps[node] ?? [];
97
+ heaps[node] = heap;
98
+ heap.push(entry);
99
+ let index = heap.length - 1;
100
+ while (index > 0) {
101
+ const parent = index - 1 >> 1;
102
+ const parentEntry = heap[parent];
103
+ if (parentEntry === void 0 || parentEntry.order >= entry.order) break;
104
+ heap[index] = parentEntry;
105
+ heap[parent] = entry;
106
+ index = parent;
107
+ }
108
+ };
109
+ const pop = (heap) => {
110
+ const last = heap.pop();
111
+ if (last === void 0 || heap.length === 0) return;
112
+ heap[0] = last;
113
+ let index = 0;
114
+ for (;;) {
115
+ const left = index * 2 + 1;
116
+ const right = left + 1;
117
+ let largest = index;
118
+ const largestOrder = () => heap[largest]?.order ?? -1;
119
+ if ((heap[left]?.order ?? -1) > largestOrder()) largest = left;
120
+ if ((heap[right]?.order ?? -1) > largestOrder()) largest = right;
121
+ const swapped = heap[largest];
122
+ if (largest === index || swapped === void 0) return;
123
+ heap[largest] = last;
124
+ heap[index] = swapped;
125
+ index = largest;
126
+ }
127
+ };
128
+ const insert = (entry) => {
129
+ let low = entry.firstColumn + leaves;
130
+ let high = entry.lastColumn + leaves + 1;
131
+ while (low < high) {
132
+ if ((low & 1) === 1) push(low++, entry);
133
+ if ((high & 1) === 1) push(--high, entry);
134
+ low >>= 1;
135
+ high >>= 1;
136
+ }
137
+ };
138
+ let nextEntry = 0;
139
+ return { at: (row, column) => {
140
+ for (let entry = clipped[nextEntry]; entry !== void 0 && entry.firstRow <= row;) {
141
+ insert(entry);
142
+ nextEntry += 1;
143
+ entry = clipped[nextEntry];
144
+ }
145
+ let best;
146
+ for (let node = column - visited.start.c + leaves; node >= 1; node >>= 1) {
147
+ const heap = heaps[node];
148
+ if (heap === void 0) continue;
149
+ while (heap[0] !== void 0 && heap[0].lastRow < row) pop(heap);
150
+ const top = heap[0];
151
+ if (top !== void 0 && (best === void 0 || top.order > best.order)) best = top;
152
+ }
153
+ return best?.link;
154
+ } };
155
+ };
156
+ //#endregion
157
+ export { makeHyperlinkLookup, resolveXlsxHyperlinks };
158
+
159
+ //# sourceMappingURL=xlsx-hyperlinks.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-hyperlinks.mjs","names":[],"sources":["../../src/node/xlsx-hyperlinks.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\nimport type { StrippedHyperlinkTags } from './office-archive.ts'\nimport { indexParts, relationships, relationshipsPathFor, utf8Text } from './xlsx-parts.ts'\nimport type { Relationship } from './xlsx-parts.ts'\nimport { parseCellRange } from './xlsx-range.ts'\nimport type { CellRange } from './xlsx-range.ts'\nimport { prefixedAttribute, xmlAttributes } from './xml-text.ts'\n\nexport type XlsxHyperlink = {\n readonly range: CellRange\n /** Normalized `http:`, `https:`, or `mailto:` URL, at most 2,048 characters. */\n readonly target: string\n /** Label for cells without text, at most 1,024 characters (longer labels end in `…`). */\n readonly display?: string\n}\n\n/** Hyperlinks per worksheet name, in document order (a later link wins on overlap). */\nexport type XlsxHyperlinks = ReadonlyMap<string, ReadonlyArray<XlsxHyperlink>>\n\n/** Longer targets are dropped: a truncated URL would point somewhere else. */\nconst maxHyperlinkTargetCharacters = 2048\n\n/** Longer display labels are cut to this many characters, ending with an ellipsis. */\nconst maxHyperlinkDisplayCharacters = 1024\n\nconst capDisplay = (display: string) => {\n if (display.length <= maxHyperlinkDisplayCharacters) return display\n\n let kept = display.slice(0, maxHyperlinkDisplayCharacters - 1)\n const last = kept.charCodeAt(kept.length - 1)\n\n // Never leave half of a surrogate pair before the ellipsis.\n if (last >= 0xd800 && last <= 0xdbff) kept = kept.slice(0, -1)\n\n return `${kept}\\u2026`\n}\n\nconst shownProtocols = new Set(['http:', 'https:', 'mailto:'])\n\nconst shownTarget = (target: string) => {\n if (target.length > maxHyperlinkTargetCharacters || !URL.canParse(target)) return undefined\n\n const url = new URL(target)\n\n if (!shownProtocols.has(url.protocol) || url.href.length > maxHyperlinkTargetCharacters)\n return undefined\n\n return url.href\n}\n\nconst hyperlinkFrom = (\n tag: string,\n sheetRelationships: ReadonlyMap<string, Relationship>\n): XlsxHyperlink | undefined => {\n // Tags were captured as Latin-1 text of the original bytes; attribute values are UTF-8.\n const attributes = xmlAttributes(Buffer.from(tag, 'latin1').toString('utf8'))\n const ref = attributes.get('ref')\n const range = ref === undefined ? undefined : parseCellRange(ref)\n const id = prefixedAttribute(attributes, 'id')\n const relationship = id === undefined ? undefined : sheetRelationships.get(id)\n\n // Internal `location`-only links (`#Sheet2!A1`) are omitted; external links keep their\n // location as a fragment, as SheetJS does.\n if (range === undefined || relationship === undefined || !relationship.external) return undefined\n\n const location = attributes.get('location')\n\n const target = shownTarget(\n location === undefined || location.length === 0\n ? relationship.target\n : `${relationship.target}#${location}`\n )\n\n if (target === undefined) return undefined\n\n const display = attributes.get('display')?.trim()\n\n return display === undefined || display.length === 0\n ? { range, target }\n : { range, target, display: capDisplay(display) }\n}\n\n/** A sheet of the workbook and the validated worksheet part SheetJS reads for it, if any. */\nexport type HyperlinkSheet = {\n readonly name: string\n readonly part: string | undefined\n}\n\n/**\n * Map the hyperlink tags removed from a workbook to its sheets (the same sheet list and worksheet\n * parts SheetJS reads, from `buildSheetJsInput`) through each worksheet's relationships.\n */\nexport const resolveXlsxHyperlinks = (\n parts: Readonly<Record<string, Uint8Array>>,\n hyperlinkTags: StrippedHyperlinkTags,\n sheets: ReadonlyArray<HyperlinkSheet>\n): XlsxHyperlinks => {\n const bySheet = new Map<string, ReadonlyArray<XlsxHyperlink>>()\n\n if (hyperlinkTags.size === 0) return bySheet\n\n const index = indexParts(parts)\n const text = (path: string) => utf8Text(index.find(path)?.bytes)\n\n for (const { name, part } of sheets) {\n const tags = part === undefined ? undefined : hyperlinkTags.get(part)\n\n if (part === undefined || tags === undefined || bySheet.has(name)) continue\n\n const sheetRelationships = relationships(text(relationshipsPathFor(part)))\n\n const links = tags.flatMap(linkTag => {\n const link = hyperlinkFrom(linkTag, sheetRelationships)\n\n return link === undefined ? [] : [link]\n })\n\n if (links.length > 0) bySheet.set(name, links)\n }\n\n return bySheet\n}\n\ntype ClippedLink = {\n readonly order: number\n readonly firstRow: number\n readonly lastRow: number\n readonly firstColumn: number\n readonly lastColumn: number\n readonly link: XlsxHyperlink\n}\n\nexport type HyperlinkLookup = {\n /** The winning link at a cell. Rows must be queried in ascending order. */\n readonly at: (row: number, column: number) => XlsxHyperlink | undefined\n}\n\n/**\n * Index links clipped to the visited range for row-major lookup without scanning every link per\n * cell: a segment tree over columns whose nodes hold max-heaps (by document order) of the links\n * covering them. Links enter when the sweep reaches their first row and are discarded lazily once\n * it passes their last row. Work is O((links + cells) · log(columns) · log(links)).\n */\nexport const makeHyperlinkLookup = (\n links: ReadonlyArray<XlsxHyperlink>,\n visited: CellRange\n): HyperlinkLookup => {\n const clipped = links\n .flatMap((link, order): ReadonlyArray<ClippedLink> => {\n const firstRow = Math.max(link.range.start.r, visited.start.r)\n const lastRow = Math.min(link.range.end.r, visited.end.r)\n const firstColumn = Math.max(link.range.start.c, visited.start.c) - visited.start.c\n const lastColumn = Math.min(link.range.end.c, visited.end.c) - visited.start.c\n\n return firstRow > lastRow || firstColumn > lastColumn\n ? []\n : [{ order, firstRow, lastRow, firstColumn, lastColumn, link }]\n })\n .sort((left, right) => left.firstRow - right.firstRow || left.order - right.order)\n\n if (clipped.length === 0) return { at: () => undefined }\n\n const width = visited.end.c - visited.start.c + 1\n let leaves = 1\n\n while (leaves < width) leaves *= 2\n\n const heaps: Array<Array<ClippedLink> | undefined> = []\n\n const push = (node: number, entry: ClippedLink) => {\n const heap = heaps[node] ?? []\n heaps[node] = heap\n heap.push(entry)\n\n let index = heap.length - 1\n\n while (index > 0) {\n const parent = (index - 1) >> 1\n const parentEntry = heap[parent]\n\n if (parentEntry === undefined || parentEntry.order >= entry.order) break\n\n heap[index] = parentEntry\n heap[parent] = entry\n index = parent\n }\n }\n\n const pop = (heap: Array<ClippedLink>) => {\n const last = heap.pop()\n\n if (last === undefined || heap.length === 0) return\n\n heap[0] = last\n\n let index = 0\n\n for (;;) {\n const left = index * 2 + 1\n const right = left + 1\n let largest = index\n\n const largestOrder = () => heap[largest]?.order ?? -1\n\n if ((heap[left]?.order ?? -1) > largestOrder()) largest = left\n\n if ((heap[right]?.order ?? -1) > largestOrder()) largest = right\n\n const swapped = heap[largest]\n\n if (largest === index || swapped === undefined) return\n\n heap[largest] = last\n heap[index] = swapped\n index = largest\n }\n }\n\n const insert = (entry: ClippedLink) => {\n let low = entry.firstColumn + leaves\n let high = entry.lastColumn + leaves + 1\n\n while (low < high) {\n if ((low & 1) === 1) push(low++, entry)\n\n if ((high & 1) === 1) push(--high, entry)\n\n low >>= 1\n high >>= 1\n }\n }\n\n let nextEntry = 0\n\n return {\n at: (row, column) => {\n for (let entry = clipped[nextEntry]; entry !== undefined && entry.firstRow <= row;) {\n insert(entry)\n nextEntry += 1\n entry = clipped[nextEntry]\n }\n\n let best: ClippedLink | undefined\n\n for (let node = column - visited.start.c + leaves; node >= 1; node >>= 1) {\n const heap = heaps[node]\n\n if (heap === undefined) continue\n\n while (heap[0] !== undefined && heap[0].lastRow < row) pop(heap)\n\n const top = heap[0]\n\n if (top !== undefined && (best === undefined || top.order > best.order)) best = top\n }\n\n return best?.link\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ //#region src/node/xlsx-parts.d.ts
2
+ /** The main workbook part every validated XLSX archive contains. */
3
+ declare const workbookPartPath = "xl/workbook.xml";
4
+ declare const utf8Text: (bytes: Uint8Array | undefined) => string;
5
+ /** Resolve a relationship target against the directory of its source part (`xl/`, …). */
6
+ declare const resolvePartPath: (baseDirectory: string, target: string) => string;
7
+ declare const directoryOf: (path: string) => string;
8
+ declare const relationshipsPathFor: (partPath: string) => string;
9
+ /**
10
+ * Validated parts by case-insensitive name, as SheetJS and OPC look them up. Archive validation
11
+ * rejects names that differ only in case, so each lower-cased name has one part.
12
+ */
13
+ type PartIndex = {
14
+ /** The validated entry name and bytes of a part, matched ignoring case. */readonly find: (path: string) => {
15
+ readonly name: string;
16
+ readonly bytes: Uint8Array;
17
+ } | undefined;
18
+ };
19
+ declare const indexParts: (parts: Readonly<Record<string, Uint8Array>>) => PartIndex;
20
+ type Relationship = {
21
+ readonly target: string; /** The `Type` attribute as written (relationship types are case-sensitive URIs). */
22
+ readonly type: string | undefined;
23
+ readonly external: boolean;
24
+ };
25
+ /** Relationships by `Id`; the first relationship with an `Id` wins. */
26
+ declare const relationships: (xml: string) => ReadonlyMap<string, Relationship>;
27
+ //#endregion
28
+ export { PartIndex, Relationship, directoryOf, indexParts, relationships, relationshipsPathFor, resolvePartPath, utf8Text, workbookPartPath };
29
+ //# sourceMappingURL=xlsx-parts.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-parts.d.mts","names":[],"sources":["../../src/node/xlsx-parts.ts"],"mappings":";;cAIa,gBAAA;AAAA,cAEA,QAAA,GAAY,KAA6B,EAAtB,UAAU;;cAI7B,eAAA,GAAmB,aAAA,UAAuB,MAAc;AAAA,cAYxD,WAAA,GAAe,IAAY;AAAA,cAE3B,oBAAA,GAAwB,QAAgB;;;;AAlBC;KAyB1C,SAAA;EAXX,oFAaU,IAAA,GAAO,IAAA;IAAA,SAA4B,IAAA;IAAA,SAAuB,KAAA,EAAO,UAAU;EAAA;AAAA;AAAA,cAGzE,UAAA,GAAc,KAAA,EAAO,QAAA,CAAS,MAAA,SAAe,UAAA,OAAe,SAAA;AAAA,KAqB7D,YAAA;EAAA,SACD,MAAA,UAlCE;EAAA,SAoCF,IAAA;EAAA,SACA,QAAA;AAAA;AArC0C;AAAA,cAyCxC,aAAA,GAAiB,GAAA,aAAc,WAAW,SAAS,YAAA"}
@@ -0,0 +1,49 @@
1
+ import { xmlAttributes } from "./xml-text.mjs";
2
+ import { Buffer } from "node:buffer";
3
+ //#region src/node/xlsx-parts.ts
4
+ /** The main workbook part every validated XLSX archive contains. */
5
+ const workbookPartPath = "xl/workbook.xml";
6
+ const utf8Text = (bytes) => bytes === void 0 ? "" : Buffer.from(bytes).toString("utf8");
7
+ /** Resolve a relationship target against the directory of its source part (`xl/`, …). */
8
+ const resolvePartPath = (baseDirectory, target) => {
9
+ const segments = [];
10
+ const path = target.startsWith("/") ? target.slice(1) : `${baseDirectory}${target}`;
11
+ for (const segment of path.split("/")) if (segment === "..") segments.pop();
12
+ else if (segment !== "." && segment !== "") segments.push(segment);
13
+ return segments.join("/");
14
+ };
15
+ const directoryOf = (path) => path.slice(0, path.lastIndexOf("/") + 1);
16
+ const relationshipsPathFor = (partPath) => `${directoryOf(partPath)}_rels/${partPath.slice(partPath.lastIndexOf("/") + 1)}.rels`;
17
+ const indexParts = (parts) => {
18
+ const names = /* @__PURE__ */ new Map();
19
+ for (const name of Object.keys(parts)) names.set(name.toLowerCase(), name);
20
+ return { find: (path) => {
21
+ const name = names.get(path.toLowerCase());
22
+ const bytes = name === void 0 ? void 0 : parts[name];
23
+ return name === void 0 || bytes === void 0 ? void 0 : {
24
+ name,
25
+ bytes
26
+ };
27
+ } };
28
+ };
29
+ const startTagPattern = (localName) => new RegExp(`<(?:[\\w.-]+:)?${localName}(?=[\\s/>])[^<>]*>`, "g");
30
+ const relationshipTag = startTagPattern("Relationship");
31
+ /** Relationships by `Id`; the first relationship with an `Id` wins. */
32
+ const relationships = (xml) => {
33
+ const byId = /* @__PURE__ */ new Map();
34
+ for (const [tag] of xml.matchAll(relationshipTag)) {
35
+ const attributes = xmlAttributes(tag);
36
+ const id = attributes.get("Id");
37
+ const target = attributes.get("Target");
38
+ if (id !== void 0 && target !== void 0 && !byId.has(id)) byId.set(id, {
39
+ target,
40
+ type: attributes.get("Type"),
41
+ external: attributes.get("TargetMode") === "External"
42
+ });
43
+ }
44
+ return byId;
45
+ };
46
+ //#endregion
47
+ export { directoryOf, indexParts, relationships, relationshipsPathFor, resolvePartPath, utf8Text, workbookPartPath };
48
+
49
+ //# sourceMappingURL=xlsx-parts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-parts.mjs","names":[],"sources":["../../src/node/xlsx-parts.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\nimport { xmlAttributes } from './xml-text.ts'\n\n/** The main workbook part every validated XLSX archive contains. */\nexport const workbookPartPath = 'xl/workbook.xml'\n\nexport const utf8Text = (bytes: Uint8Array | undefined) =>\n bytes === undefined ? '' : Buffer.from(bytes).toString('utf8')\n\n/** Resolve a relationship target against the directory of its source part (`xl/`, …). */\nexport const resolvePartPath = (baseDirectory: string, target: string) => {\n const segments: Array<string> = []\n const path = target.startsWith('/') ? target.slice(1) : `${baseDirectory}${target}`\n\n for (const segment of path.split('/')) {\n if (segment === '..') segments.pop()\n else if (segment !== '.' && segment !== '') segments.push(segment)\n }\n\n return segments.join('/')\n}\n\nexport const directoryOf = (path: string) => path.slice(0, path.lastIndexOf('/') + 1)\n\nexport const relationshipsPathFor = (partPath: string) =>\n `${directoryOf(partPath)}_rels/${partPath.slice(partPath.lastIndexOf('/') + 1)}.rels`\n\n/**\n * Validated parts by case-insensitive name, as SheetJS and OPC look them up. Archive validation\n * rejects names that differ only in case, so each lower-cased name has one part.\n */\nexport type PartIndex = {\n /** The validated entry name and bytes of a part, matched ignoring case. */\n readonly find: (path: string) => { readonly name: string; readonly bytes: Uint8Array } | undefined\n}\n\nexport const indexParts = (parts: Readonly<Record<string, Uint8Array>>): PartIndex => {\n const names = new Map<string, string>()\n\n for (const name of Object.keys(parts)) names.set(name.toLowerCase(), name)\n\n return {\n find: path => {\n const name = names.get(path.toLowerCase())\n const bytes = name === undefined ? undefined : parts[name]\n\n return name === undefined || bytes === undefined ? undefined : { name, bytes }\n }\n }\n}\n\n// `[^<>]` keeps each candidate inside one tag, so scans stay linear in the part size.\nconst startTagPattern = (localName: string) =>\n new RegExp(`<(?:[\\\\w.-]+:)?${localName}(?=[\\\\s/>])[^<>]*>`, 'g')\n\nconst relationshipTag = startTagPattern('Relationship')\n\nexport type Relationship = {\n readonly target: string\n /** The `Type` attribute as written (relationship types are case-sensitive URIs). */\n readonly type: string | undefined\n readonly external: boolean\n}\n\n/** Relationships by `Id`; the first relationship with an `Id` wins. */\nexport const relationships = (xml: string): ReadonlyMap<string, Relationship> => {\n const byId = new Map<string, Relationship>()\n\n for (const [tag] of xml.matchAll(relationshipTag)) {\n const attributes = xmlAttributes(tag)\n const id = attributes.get('Id')\n const target = attributes.get('Target')\n\n if (id !== undefined && target !== undefined && !byId.has(id))\n byId.set(id, {\n target,\n type: attributes.get('Type'),\n external: attributes.get('TargetMode') === 'External'\n })\n }\n\n return byId\n}\n"],"mappings":";;;;AAIA,MAAa,mBAAmB;AAEhC,MAAa,YAAY,UACvB,UAAU,KAAA,IAAY,KAAK,OAAO,KAAK,KAAK,EAAE,SAAS,MAAM;;AAG/D,MAAa,mBAAmB,eAAuB,WAAmB;CACxE,MAAM,WAA0B,CAAC;CACjC,MAAM,OAAO,OAAO,WAAW,GAAG,IAAI,OAAO,MAAM,CAAC,IAAI,GAAG,gBAAgB;CAE3E,KAAK,MAAM,WAAW,KAAK,MAAM,GAAG,GAClC,IAAI,YAAY,MAAM,SAAS,IAAI;MAC9B,IAAI,YAAY,OAAO,YAAY,IAAI,SAAS,KAAK,OAAO;CAGnE,OAAO,SAAS,KAAK,GAAG;AAC1B;AAEA,MAAa,eAAe,SAAiB,KAAK,MAAM,GAAG,KAAK,YAAY,GAAG,IAAI,CAAC;AAEpF,MAAa,wBAAwB,aACnC,GAAG,YAAY,QAAQ,EAAE,QAAQ,SAAS,MAAM,SAAS,YAAY,GAAG,IAAI,CAAC,EAAE;AAWjF,MAAa,cAAc,UAA2D;CACpF,MAAM,wBAAQ,IAAI,IAAoB;CAEtC,KAAK,MAAM,QAAQ,OAAO,KAAK,KAAK,GAAG,MAAM,IAAI,KAAK,YAAY,GAAG,IAAI;CAEzE,OAAO,EACL,OAAM,SAAQ;EACZ,MAAM,OAAO,MAAM,IAAI,KAAK,YAAY,CAAC;EACzC,MAAM,QAAQ,SAAS,KAAA,IAAY,KAAA,IAAY,MAAM;EAErD,OAAO,SAAS,KAAA,KAAa,UAAU,KAAA,IAAY,KAAA,IAAY;GAAE;GAAM;EAAM;CAC/E,EACF;AACF;AAGA,MAAM,mBAAmB,cACvB,IAAI,OAAO,kBAAkB,UAAU,qBAAqB,GAAG;AAEjE,MAAM,kBAAkB,gBAAgB,cAAc;;AAUtD,MAAa,iBAAiB,QAAmD;CAC/E,MAAM,uBAAO,IAAI,IAA0B;CAE3C,KAAK,MAAM,CAAC,QAAQ,IAAI,SAAS,eAAe,GAAG;EACjD,MAAM,aAAa,cAAc,GAAG;EACpC,MAAM,KAAK,WAAW,IAAI,IAAI;EAC9B,MAAM,SAAS,WAAW,IAAI,QAAQ;EAEtC,IAAI,OAAO,KAAA,KAAa,WAAW,KAAA,KAAa,CAAC,KAAK,IAAI,EAAE,GAC1D,KAAK,IAAI,IAAI;GACX;GACA,MAAM,WAAW,IAAI,MAAM;GAC3B,UAAU,WAAW,IAAI,YAAY,MAAM;EAC7C,CAAC;CACL;CAEA,OAAO;AACT"}
@@ -0,0 +1,21 @@
1
+ //#region src/node/xlsx-range.d.ts
2
+ /** Zero-based cell position. */
3
+ type CellPosition = {
4
+ readonly r: number;
5
+ readonly c: number;
6
+ };
7
+ type CellRange = {
8
+ readonly start: CellPosition;
9
+ readonly end: CellPosition;
10
+ };
11
+ /**
12
+ * Strictly parse `A1` or `A1:B2` within Excel's grid. Permissive SheetJS range decoders wrap or
13
+ * normalize invalid and overflowed references, so they are never used on untrusted refs.
14
+ */
15
+ declare const parseCellRange: (ref: string) => CellRange | undefined;
16
+ declare const cellCount: (range: CellRange) => number;
17
+ /** `A1`-style address of a zero-based position. */
18
+ declare const encodeCellAddress: (position: CellPosition) => string;
19
+ //#endregion
20
+ export { CellPosition, CellRange, cellCount, encodeCellAddress, parseCellRange };
21
+ //# sourceMappingURL=xlsx-range.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-range.d.mts","names":[],"sources":["../../src/node/xlsx-range.ts"],"mappings":";;KACY,YAAA;EAAA,SACD,CAAA;EAAA,SACA,CAAC;AAAA;AAAA,KAGA,SAAA;EAAA,SACD,KAAA,EAAO,YAAA;EAAA,SACP,GAAA,EAAK,YAAY;AAAA;;;;;cA6Bf,cAAA,GAAkB,GAAA,aAAc,SAAS;AAAA,cAiBzC,SAAA,GAAa,KAAgB,EAAT,SAAS;AA9Cd;AAAA,cAkDf,iBAAA,GAAqB,QAAsB,EAAZ,YAAY"}
@@ -0,0 +1,49 @@
1
+ //#region src/node/xlsx-range.ts
2
+ const maxExcelRows = 1048576;
3
+ const maxExcelColumns = 16384;
4
+ const cellAddressPattern = /^([A-Z]{1,3})([1-9][0-9]{0,6})$/;
5
+ const cellAddress = (value) => {
6
+ const match = cellAddressPattern.exec(value);
7
+ if (match?.[1] === void 0 || match[2] === void 0) return void 0;
8
+ let column = 0;
9
+ for (const letter of match[1]) column = column * 26 + letter.charCodeAt(0) - 64;
10
+ const row = Number(match[2]);
11
+ if (!Number.isSafeInteger(row) || row > maxExcelRows || column > maxExcelColumns) return void 0;
12
+ return {
13
+ r: row - 1,
14
+ c: column - 1
15
+ };
16
+ };
17
+ /**
18
+ * Strictly parse `A1` or `A1:B2` within Excel's grid. Permissive SheetJS range decoders wrap or
19
+ * normalize invalid and overflowed references, so they are never used on untrusted refs.
20
+ */
21
+ const parseCellRange = (ref) => {
22
+ if (ref.length > 21) return void 0;
23
+ const parts = ref.split(":");
24
+ const first = parts[0];
25
+ if (first === void 0 || parts.length > 2) return void 0;
26
+ const start = cellAddress(first);
27
+ const end = cellAddress(parts[1] ?? first);
28
+ if (start === void 0 || end === void 0 || end.r < start.r || end.c < start.c) return void 0;
29
+ return {
30
+ start,
31
+ end
32
+ };
33
+ };
34
+ const cellCount = (range) => (range.end.r - range.start.r + 1) * (range.end.c - range.start.c + 1);
35
+ /** `A1`-style address of a zero-based position. */
36
+ const encodeCellAddress = (position) => {
37
+ let column = "";
38
+ let remaining = position.c + 1;
39
+ while (remaining > 0) {
40
+ const letter = (remaining - 1) % 26;
41
+ column = String.fromCharCode(65 + letter) + column;
42
+ remaining = Math.floor((remaining - 1) / 26);
43
+ }
44
+ return `${column}${position.r + 1}`;
45
+ };
46
+ //#endregion
47
+ export { cellCount, encodeCellAddress, parseCellRange };
48
+
49
+ //# sourceMappingURL=xlsx-range.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-range.mjs","names":[],"sources":["../../src/node/xlsx-range.ts"],"sourcesContent":["/** Zero-based cell position. */\nexport type CellPosition = {\n readonly r: number\n readonly c: number\n}\n\nexport type CellRange = {\n readonly start: CellPosition\n readonly end: CellPosition\n}\n\nconst maxExcelRows = 1_048_576\n\nconst maxExcelColumns = 16_384\n\nconst cellAddressPattern = /^([A-Z]{1,3})([1-9][0-9]{0,6})$/\n\nconst cellAddress = (value: string): CellPosition | undefined => {\n const match = cellAddressPattern.exec(value)\n\n if (match?.[1] === undefined || match[2] === undefined) return undefined\n\n let column = 0\n\n for (const letter of match[1]) column = column * 26 + letter.charCodeAt(0) - 64\n\n const row = Number(match[2])\n\n if (!Number.isSafeInteger(row) || row > maxExcelRows || column > maxExcelColumns) return undefined\n\n return { r: row - 1, c: column - 1 }\n}\n\n/**\n * Strictly parse `A1` or `A1:B2` within Excel's grid. Permissive SheetJS range decoders wrap or\n * normalize invalid and overflowed references, so they are never used on untrusted refs.\n */\nexport const parseCellRange = (ref: string): CellRange | undefined => {\n if (ref.length > 21) return undefined\n\n const parts = ref.split(':')\n const first = parts[0]\n\n if (first === undefined || parts.length > 2) return undefined\n\n const start = cellAddress(first)\n const end = cellAddress(parts[1] ?? first)\n\n if (start === undefined || end === undefined || end.r < start.r || end.c < start.c)\n return undefined\n\n return { start, end }\n}\n\nexport const cellCount = (range: CellRange) =>\n (range.end.r - range.start.r + 1) * (range.end.c - range.start.c + 1)\n\n/** `A1`-style address of a zero-based position. */\nexport const encodeCellAddress = (position: CellPosition) => {\n let column = ''\n let remaining = position.c + 1\n\n while (remaining > 0) {\n const letter = (remaining - 1) % 26\n column = String.fromCharCode(65 + letter) + column\n remaining = Math.floor((remaining - 1) / 26)\n }\n\n return `${column}${position.r + 1}`\n}\n"],"mappings":";AAWA,MAAM,eAAe;AAErB,MAAM,kBAAkB;AAExB,MAAM,qBAAqB;AAE3B,MAAM,eAAe,UAA4C;CAC/D,MAAM,QAAQ,mBAAmB,KAAK,KAAK;CAE3C,IAAI,QAAQ,OAAO,KAAA,KAAa,MAAM,OAAO,KAAA,GAAW,OAAO,KAAA;CAE/D,IAAI,SAAS;CAEb,KAAK,MAAM,UAAU,MAAM,IAAI,SAAS,SAAS,KAAK,OAAO,WAAW,CAAC,IAAI;CAE7E,MAAM,MAAM,OAAO,MAAM,EAAE;CAE3B,IAAI,CAAC,OAAO,cAAc,GAAG,KAAK,MAAM,gBAAgB,SAAS,iBAAiB,OAAO,KAAA;CAEzF,OAAO;EAAE,GAAG,MAAM;EAAG,GAAG,SAAS;CAAE;AACrC;;;;;AAMA,MAAa,kBAAkB,QAAuC;CACpE,IAAI,IAAI,SAAS,IAAI,OAAO,KAAA;CAE5B,MAAM,QAAQ,IAAI,MAAM,GAAG;CAC3B,MAAM,QAAQ,MAAM;CAEpB,IAAI,UAAU,KAAA,KAAa,MAAM,SAAS,GAAG,OAAO,KAAA;CAEpD,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,MAAM,YAAY,MAAM,MAAM,KAAK;CAEzC,IAAI,UAAU,KAAA,KAAa,QAAQ,KAAA,KAAa,IAAI,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,GAC/E,OAAO,KAAA;CAET,OAAO;EAAE;EAAO;CAAI;AACtB;AAEA,MAAa,aAAa,WACvB,MAAM,IAAI,IAAI,MAAM,MAAM,IAAI,MAAM,MAAM,IAAI,IAAI,MAAM,MAAM,IAAI;;AAGrE,MAAa,qBAAqB,aAA2B;CAC3D,IAAI,SAAS;CACb,IAAI,YAAY,SAAS,IAAI;CAE7B,OAAO,YAAY,GAAG;EACpB,MAAM,UAAU,YAAY,KAAK;EACjC,SAAS,OAAO,aAAa,KAAK,MAAM,IAAI;EAC5C,YAAY,KAAK,OAAO,YAAY,KAAK,EAAE;CAC7C;CAEA,OAAO,GAAG,SAAS,SAAS,IAAI;AAClC"}
@@ -0,0 +1,36 @@
1
+ //#region src/node/xlsx-routing.d.ts
2
+ /**
3
+ * Early, exact rejection of XLSX input that SheetJS 0.20.3 would hand to its ODS, Numbers, or
4
+ * binary (XLSB) parsers, so users get a clear error instead of "Could not read XLSX". These checks
5
+ * are not the security guarantee: SheetJS only ever receives the allowlisted archive built by
6
+ * `buildSheetJsInput` (`xlsx-sheetjs-input.ts`), which contains no marker entries and no `.bin`
7
+ * parts whatever these checks decide.
8
+ */
9
+ /**
10
+ * An entry name as SheetJS looks it up: its ZIP reader (`cfb_add`) keeps a name that already
11
+ * starts with `Root Entry/` and otherwise stores `"Root Entry/" + name` with the first `//`
12
+ * collapsed; `safegetzipfile` strips `Root Entry/`, treats `\` and `/` alike, and compares
13
+ * lower-cased names.
14
+ */
15
+ declare const sheetJsEntryPath: (name: string) => string;
16
+ /**
17
+ * Whether SheetJS would treat this entry as an ODS, UOC, or Numbers marker. `CFB.find` matches
18
+ * `Index.zip` by base name, and any `Root Entry/` name (any case) is rejected outright.
19
+ */
20
+ declare const isAlternateFormatEntry: (name: string) => boolean;
21
+ /**
22
+ * Whether a relationships part has a `<Relationship>` whose `Target` ends in `.bin` and whose
23
+ * `Type` (read exactly as SheetJS reads it: case-sensitive, missing counts as a sheet) is not on
24
+ * the allowlist.
25
+ */
26
+ declare const relationshipsRouteToBinary: (content: Uint8Array) => boolean;
27
+ /**
28
+ * Whether `[Content_Types].xml` has an `<Override>` (any prefix, as SheetJS reads it) with an
29
+ * XLSB content type, or a `PartName` ending in `.bin` whose content type is not on the allowlist.
30
+ * `<Default>` entries are ignored: SheetJS does not route by them, and its own XLSX writer emits
31
+ * `<Default Extension="bin">` with the XLSB workbook type.
32
+ */
33
+ declare const contentTypesRouteToBinary: (content: Uint8Array) => boolean;
34
+ //#endregion
35
+ export { contentTypesRouteToBinary, isAlternateFormatEntry, relationshipsRouteToBinary, sheetJsEntryPath };
36
+ //# sourceMappingURL=xlsx-routing.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-routing.d.mts","names":[],"sources":["../../src/node/xlsx-routing.ts"],"mappings":";;AAiBA;;;;AAA6C;AAiB7C;;;;AAAmD;AA0FnD;;cA3Ga,gBAAA,GAAoB,IAAY;;AA2GiB;AAgC9D;;cA1Ha,sBAAA,GAA0B,IAAY;;AA0HU;;;;cAhChD,0BAAA,GAA8B,OAAmB,EAAV,UAAU;;;;;;;cAgCjD,yBAAA,GAA6B,OAAmB,EAAV,UAAU"}