Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: switch from SheetJS to ExcelJS for proper style extraction

SheetJS community edition (xlsx v0.18.5) silently ignores cellStyles —
background colors, text colors, and font properties were never extracted.
ExcelJS (MIT licensed) properly supports all cell styles.

Import now extracts: bold, italic, underline, strikethrough, font size,
text color (ARGB), background color (pattern fill), horizontal/vertical
alignment, wrap text, and number formats.

Export now writes: all the same styles back to Excel format.

Tests rewritten from mock-based to end-to-end: create real ExcelJS
workbooks with styled cells, export to buffer, parse with import
function, verify styles round-trip correctly.

+1149 -658
+690 -93
package-lock.json
··· 34 34 "better-sqlite3": "^11.7.0", 35 35 "chart.js": "^4.5.1", 36 36 "compression": "^1.7.5", 37 + "exceljs": "^4.4.0", 37 38 "express": "^4.21.0", 38 39 "html2pdf.js": "^0.14.0", 39 40 "lib0": "^0.2.99", ··· 42 43 "turndown": "^7.2.2", 43 44 "turndown-plugin-gfm": "^1.0.2", 44 45 "ws": "^8.18.0", 45 - "xlsx": "^0.18.5", 46 46 "y-prosemirror": "^1.2.15", 47 47 "yjs": "^13.6.20" 48 48 }, ··· 725 725 "optional": true 726 726 } 727 727 } 728 + }, 729 + "node_modules/@fast-csv/format": { 730 + "version": "4.3.5", 731 + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", 732 + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", 733 + "license": "MIT", 734 + "dependencies": { 735 + "@types/node": "^14.0.1", 736 + "lodash.escaperegexp": "^4.1.2", 737 + "lodash.isboolean": "^3.0.3", 738 + "lodash.isequal": "^4.5.0", 739 + "lodash.isfunction": "^3.0.9", 740 + "lodash.isnil": "^4.0.0" 741 + } 742 + }, 743 + "node_modules/@fast-csv/format/node_modules/@types/node": { 744 + "version": "14.18.63", 745 + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", 746 + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", 747 + "license": "MIT" 748 + }, 749 + "node_modules/@fast-csv/parse": { 750 + "version": "4.3.6", 751 + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", 752 + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", 753 + "license": "MIT", 754 + "dependencies": { 755 + "@types/node": "^14.0.1", 756 + "lodash.escaperegexp": "^4.1.2", 757 + "lodash.groupby": "^4.6.0", 758 + "lodash.isfunction": "^3.0.9", 759 + "lodash.isnil": "^4.0.0", 760 + "lodash.isundefined": "^3.0.1", 761 + "lodash.uniq": "^4.5.0" 762 + } 763 + }, 764 + "node_modules/@fast-csv/parse/node_modules/@types/node": { 765 + "version": "14.18.63", 766 + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", 767 + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", 768 + "license": "MIT" 728 769 }, 729 770 "node_modules/@jridgewell/sourcemap-codec": { 730 771 "version": "1.5.5", ··· 2039 2080 "node": ">= 0.6" 2040 2081 } 2041 2082 }, 2042 - "node_modules/adler-32": { 2043 - "version": "1.3.1", 2044 - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", 2045 - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", 2046 - "license": "Apache-2.0", 2047 - "engines": { 2048 - "node": ">=0.8" 2049 - } 2050 - }, 2051 2083 "node_modules/ansi-regex": { 2052 2084 "version": "5.0.1", 2053 2085 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", ··· 2074 2106 "url": "https://github.com/chalk/ansi-styles?sponsor=1" 2075 2107 } 2076 2108 }, 2109 + "node_modules/archiver": { 2110 + "version": "5.3.2", 2111 + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", 2112 + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", 2113 + "license": "MIT", 2114 + "dependencies": { 2115 + "archiver-utils": "^2.1.0", 2116 + "async": "^3.2.4", 2117 + "buffer-crc32": "^0.2.1", 2118 + "readable-stream": "^3.6.0", 2119 + "readdir-glob": "^1.1.2", 2120 + "tar-stream": "^2.2.0", 2121 + "zip-stream": "^4.1.0" 2122 + }, 2123 + "engines": { 2124 + "node": ">= 10" 2125 + } 2126 + }, 2127 + "node_modules/archiver-utils": { 2128 + "version": "2.1.0", 2129 + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", 2130 + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", 2131 + "license": "MIT", 2132 + "dependencies": { 2133 + "glob": "^7.1.4", 2134 + "graceful-fs": "^4.2.0", 2135 + "lazystream": "^1.0.0", 2136 + "lodash.defaults": "^4.2.0", 2137 + "lodash.difference": "^4.5.0", 2138 + "lodash.flatten": "^4.4.0", 2139 + "lodash.isplainobject": "^4.0.6", 2140 + "lodash.union": "^4.6.0", 2141 + "normalize-path": "^3.0.0", 2142 + "readable-stream": "^2.0.0" 2143 + }, 2144 + "engines": { 2145 + "node": ">= 6" 2146 + } 2147 + }, 2148 + "node_modules/archiver-utils/node_modules/readable-stream": { 2149 + "version": "2.3.8", 2150 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 2151 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 2152 + "license": "MIT", 2153 + "dependencies": { 2154 + "core-util-is": "~1.0.0", 2155 + "inherits": "~2.0.3", 2156 + "isarray": "~1.0.0", 2157 + "process-nextick-args": "~2.0.0", 2158 + "safe-buffer": "~5.1.1", 2159 + "string_decoder": "~1.1.1", 2160 + "util-deprecate": "~1.0.1" 2161 + } 2162 + }, 2163 + "node_modules/archiver-utils/node_modules/safe-buffer": { 2164 + "version": "5.1.2", 2165 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 2166 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 2167 + "license": "MIT" 2168 + }, 2169 + "node_modules/archiver-utils/node_modules/string_decoder": { 2170 + "version": "1.1.1", 2171 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 2172 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 2173 + "license": "MIT", 2174 + "dependencies": { 2175 + "safe-buffer": "~5.1.0" 2176 + } 2177 + }, 2077 2178 "node_modules/argparse": { 2078 2179 "version": "2.0.1", 2079 2180 "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", ··· 2095 2196 "engines": { 2096 2197 "node": ">=12" 2097 2198 } 2199 + }, 2200 + "node_modules/async": { 2201 + "version": "3.2.6", 2202 + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", 2203 + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", 2204 + "license": "MIT" 2205 + }, 2206 + "node_modules/balanced-match": { 2207 + "version": "1.0.2", 2208 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 2209 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 2210 + "license": "MIT" 2098 2211 }, 2099 2212 "node_modules/base64-arraybuffer": { 2100 2213 "version": "1.0.2", ··· 2146 2259 "require-from-string": "^2.0.2" 2147 2260 } 2148 2261 }, 2262 + "node_modules/big-integer": { 2263 + "version": "1.6.52", 2264 + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", 2265 + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", 2266 + "license": "Unlicense", 2267 + "engines": { 2268 + "node": ">=0.6" 2269 + } 2270 + }, 2271 + "node_modules/binary": { 2272 + "version": "0.3.0", 2273 + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", 2274 + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", 2275 + "license": "MIT", 2276 + "dependencies": { 2277 + "buffers": "~0.1.1", 2278 + "chainsaw": "~0.1.0" 2279 + }, 2280 + "engines": { 2281 + "node": "*" 2282 + } 2283 + }, 2149 2284 "node_modules/bindings": { 2150 2285 "version": "1.5.0", 2151 2286 "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", ··· 2196 2331 "npm": "1.2.8000 || >= 1.4.16" 2197 2332 } 2198 2333 }, 2334 + "node_modules/brace-expansion": { 2335 + "version": "1.1.12", 2336 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 2337 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 2338 + "license": "MIT", 2339 + "dependencies": { 2340 + "balanced-match": "^1.0.0", 2341 + "concat-map": "0.0.1" 2342 + } 2343 + }, 2199 2344 "node_modules/buffer": { 2200 2345 "version": "5.7.1", 2201 2346 "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", ··· 2218 2363 "dependencies": { 2219 2364 "base64-js": "^1.3.1", 2220 2365 "ieee754": "^1.1.13" 2366 + } 2367 + }, 2368 + "node_modules/buffer-crc32": { 2369 + "version": "0.2.13", 2370 + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 2371 + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 2372 + "license": "MIT", 2373 + "engines": { 2374 + "node": "*" 2375 + } 2376 + }, 2377 + "node_modules/buffer-indexof-polyfill": { 2378 + "version": "1.0.2", 2379 + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", 2380 + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", 2381 + "license": "MIT", 2382 + "engines": { 2383 + "node": ">=0.10" 2384 + } 2385 + }, 2386 + "node_modules/buffers": { 2387 + "version": "0.1.1", 2388 + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", 2389 + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", 2390 + "engines": { 2391 + "node": ">=0.2.0" 2221 2392 } 2222 2393 }, 2223 2394 "node_modules/bytes": { ··· 2278 2449 "node": ">=10.0.0" 2279 2450 } 2280 2451 }, 2281 - "node_modules/cfb": { 2282 - "version": "1.2.2", 2283 - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", 2284 - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", 2285 - "license": "Apache-2.0", 2286 - "dependencies": { 2287 - "adler-32": "~1.3.0", 2288 - "crc-32": "~1.2.0" 2289 - }, 2290 - "engines": { 2291 - "node": ">=0.8" 2292 - } 2293 - }, 2294 2452 "node_modules/chai": { 2295 2453 "version": "6.2.2", 2296 2454 "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", ··· 2299 2457 "license": "MIT", 2300 2458 "engines": { 2301 2459 "node": ">=18" 2460 + } 2461 + }, 2462 + "node_modules/chainsaw": { 2463 + "version": "0.1.0", 2464 + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", 2465 + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", 2466 + "license": "MIT/X11", 2467 + "dependencies": { 2468 + "traverse": ">=0.3.0 <0.4" 2469 + }, 2470 + "engines": { 2471 + "node": "*" 2302 2472 } 2303 2473 }, 2304 2474 "node_modules/chalk": { ··· 2364 2534 "node": ">=12" 2365 2535 } 2366 2536 }, 2367 - "node_modules/codepage": { 2368 - "version": "1.15.0", 2369 - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", 2370 - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", 2371 - "license": "Apache-2.0", 2372 - "engines": { 2373 - "node": ">=0.8" 2374 - } 2375 - }, 2376 2537 "node_modules/color-convert": { 2377 2538 "version": "2.0.1", 2378 2539 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", ··· 2393 2554 "dev": true, 2394 2555 "license": "MIT" 2395 2556 }, 2557 + "node_modules/compress-commons": { 2558 + "version": "4.1.2", 2559 + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", 2560 + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", 2561 + "license": "MIT", 2562 + "dependencies": { 2563 + "buffer-crc32": "^0.2.13", 2564 + "crc32-stream": "^4.0.2", 2565 + "normalize-path": "^3.0.0", 2566 + "readable-stream": "^3.6.0" 2567 + }, 2568 + "engines": { 2569 + "node": ">= 10" 2570 + } 2571 + }, 2396 2572 "node_modules/compressible": { 2397 2573 "version": "2.0.18", 2398 2574 "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", ··· 2423 2599 "node": ">= 0.8.0" 2424 2600 } 2425 2601 }, 2602 + "node_modules/concat-map": { 2603 + "version": "0.0.1", 2604 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 2605 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 2606 + "license": "MIT" 2607 + }, 2426 2608 "node_modules/concurrently": { 2427 2609 "version": "9.2.1", 2428 2610 "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", ··· 2521 2703 "node": ">=0.8" 2522 2704 } 2523 2705 }, 2706 + "node_modules/crc32-stream": { 2707 + "version": "4.0.3", 2708 + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", 2709 + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", 2710 + "license": "MIT", 2711 + "dependencies": { 2712 + "crc-32": "^1.2.0", 2713 + "readable-stream": "^3.4.0" 2714 + }, 2715 + "engines": { 2716 + "node": ">= 10" 2717 + } 2718 + }, 2524 2719 "node_modules/crelt": { 2525 2720 "version": "1.0.6", 2526 2721 "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", ··· 2563 2758 "engines": { 2564 2759 "node": "^20.19.0 || ^22.12.0 || >=24.0.0" 2565 2760 } 2761 + }, 2762 + "node_modules/dayjs": { 2763 + "version": "1.11.20", 2764 + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", 2765 + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", 2766 + "license": "MIT" 2566 2767 }, 2567 2768 "node_modules/debug": { 2568 2769 "version": "2.6.9", ··· 2670 2871 "node": ">= 0.4" 2671 2872 } 2672 2873 }, 2874 + "node_modules/duplexer2": { 2875 + "version": "0.1.4", 2876 + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", 2877 + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", 2878 + "license": "BSD-3-Clause", 2879 + "dependencies": { 2880 + "readable-stream": "^2.0.2" 2881 + } 2882 + }, 2883 + "node_modules/duplexer2/node_modules/readable-stream": { 2884 + "version": "2.3.8", 2885 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 2886 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 2887 + "license": "MIT", 2888 + "dependencies": { 2889 + "core-util-is": "~1.0.0", 2890 + "inherits": "~2.0.3", 2891 + "isarray": "~1.0.0", 2892 + "process-nextick-args": "~2.0.0", 2893 + "safe-buffer": "~5.1.1", 2894 + "string_decoder": "~1.1.1", 2895 + "util-deprecate": "~1.0.1" 2896 + } 2897 + }, 2898 + "node_modules/duplexer2/node_modules/safe-buffer": { 2899 + "version": "5.1.2", 2900 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 2901 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 2902 + "license": "MIT" 2903 + }, 2904 + "node_modules/duplexer2/node_modules/string_decoder": { 2905 + "version": "1.1.1", 2906 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 2907 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 2908 + "license": "MIT", 2909 + "dependencies": { 2910 + "safe-buffer": "~5.1.0" 2911 + } 2912 + }, 2673 2913 "node_modules/ee-first": { 2674 2914 "version": "1.1.1", 2675 2915 "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", ··· 2839 3079 "node": ">= 0.6" 2840 3080 } 2841 3081 }, 3082 + "node_modules/exceljs": { 3083 + "version": "4.4.0", 3084 + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", 3085 + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", 3086 + "license": "MIT", 3087 + "dependencies": { 3088 + "archiver": "^5.0.0", 3089 + "dayjs": "^1.8.34", 3090 + "fast-csv": "^4.3.1", 3091 + "jszip": "^3.10.1", 3092 + "readable-stream": "^3.6.0", 3093 + "saxes": "^5.0.1", 3094 + "tmp": "^0.2.0", 3095 + "unzipper": "^0.10.11", 3096 + "uuid": "^8.3.0" 3097 + }, 3098 + "engines": { 3099 + "node": ">=8.3.0" 3100 + } 3101 + }, 3102 + "node_modules/exceljs/node_modules/saxes": { 3103 + "version": "5.0.1", 3104 + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", 3105 + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", 3106 + "license": "ISC", 3107 + "dependencies": { 3108 + "xmlchars": "^2.2.0" 3109 + }, 3110 + "engines": { 3111 + "node": ">=10" 3112 + } 3113 + }, 2842 3114 "node_modules/expand-template": { 2843 3115 "version": "2.0.3", 2844 3116 "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", ··· 2904 3176 "url": "https://opencollective.com/express" 2905 3177 } 2906 3178 }, 3179 + "node_modules/fast-csv": { 3180 + "version": "4.3.6", 3181 + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", 3182 + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", 3183 + "license": "MIT", 3184 + "dependencies": { 3185 + "@fast-csv/format": "4.3.5", 3186 + "@fast-csv/parse": "4.3.6" 3187 + }, 3188 + "engines": { 3189 + "node": ">=10.0.0" 3190 + } 3191 + }, 2907 3192 "node_modules/fast-png": { 2908 3193 "version": "6.4.0", 2909 3194 "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", ··· 2972 3257 "node": ">= 0.6" 2973 3258 } 2974 3259 }, 2975 - "node_modules/frac": { 2976 - "version": "1.1.2", 2977 - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", 2978 - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", 2979 - "license": "Apache-2.0", 2980 - "engines": { 2981 - "node": ">=0.8" 2982 - } 2983 - }, 2984 3260 "node_modules/fresh": { 2985 3261 "version": "0.5.2", 2986 3262 "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", ··· 2996 3272 "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 2997 3273 "license": "MIT" 2998 3274 }, 3275 + "node_modules/fs.realpath": { 3276 + "version": "1.0.0", 3277 + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 3278 + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 3279 + "license": "ISC" 3280 + }, 2999 3281 "node_modules/fsevents": { 3000 3282 "version": "2.3.3", 3001 3283 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", ··· 3011 3293 "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 3012 3294 } 3013 3295 }, 3296 + "node_modules/fstream": { 3297 + "version": "1.0.12", 3298 + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", 3299 + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", 3300 + "deprecated": "This package is no longer supported.", 3301 + "license": "ISC", 3302 + "dependencies": { 3303 + "graceful-fs": "^4.1.2", 3304 + "inherits": "~2.0.0", 3305 + "mkdirp": ">=0.5 0", 3306 + "rimraf": "2" 3307 + }, 3308 + "engines": { 3309 + "node": ">=0.6" 3310 + } 3311 + }, 3014 3312 "node_modules/function-bind": { 3015 3313 "version": "1.1.2", 3016 3314 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", ··· 3086 3384 "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 3087 3385 "license": "MIT" 3088 3386 }, 3387 + "node_modules/glob": { 3388 + "version": "7.2.3", 3389 + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 3390 + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 3391 + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", 3392 + "license": "ISC", 3393 + "dependencies": { 3394 + "fs.realpath": "^1.0.0", 3395 + "inflight": "^1.0.4", 3396 + "inherits": "2", 3397 + "minimatch": "^3.1.1", 3398 + "once": "^1.3.0", 3399 + "path-is-absolute": "^1.0.0" 3400 + }, 3401 + "engines": { 3402 + "node": "*" 3403 + }, 3404 + "funding": { 3405 + "url": "https://github.com/sponsors/isaacs" 3406 + } 3407 + }, 3089 3408 "node_modules/gopd": { 3090 3409 "version": "1.2.0", 3091 3410 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", ··· 3097 3416 "funding": { 3098 3417 "url": "https://github.com/sponsors/ljharb" 3099 3418 } 3419 + }, 3420 + "node_modules/graceful-fs": { 3421 + "version": "4.2.11", 3422 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 3423 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 3424 + "license": "ISC" 3100 3425 }, 3101 3426 "node_modules/has-flag": { 3102 3427 "version": "4.0.0", ··· 3227 3552 "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", 3228 3553 "license": "MIT" 3229 3554 }, 3555 + "node_modules/inflight": { 3556 + "version": "1.0.6", 3557 + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 3558 + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 3559 + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 3560 + "license": "ISC", 3561 + "dependencies": { 3562 + "once": "^1.3.0", 3563 + "wrappy": "1" 3564 + } 3565 + }, 3230 3566 "node_modules/inherits": { 3231 3567 "version": "2.0.4", 3232 3568 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", ··· 3394 3730 "safe-buffer": "~5.1.0" 3395 3731 } 3396 3732 }, 3733 + "node_modules/lazystream": { 3734 + "version": "1.0.1", 3735 + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", 3736 + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", 3737 + "license": "MIT", 3738 + "dependencies": { 3739 + "readable-stream": "^2.0.5" 3740 + }, 3741 + "engines": { 3742 + "node": ">= 0.6.3" 3743 + } 3744 + }, 3745 + "node_modules/lazystream/node_modules/readable-stream": { 3746 + "version": "2.3.8", 3747 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 3748 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 3749 + "license": "MIT", 3750 + "dependencies": { 3751 + "core-util-is": "~1.0.0", 3752 + "inherits": "~2.0.3", 3753 + "isarray": "~1.0.0", 3754 + "process-nextick-args": "~2.0.0", 3755 + "safe-buffer": "~5.1.1", 3756 + "string_decoder": "~1.1.1", 3757 + "util-deprecate": "~1.0.1" 3758 + } 3759 + }, 3760 + "node_modules/lazystream/node_modules/safe-buffer": { 3761 + "version": "5.1.2", 3762 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 3763 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 3764 + "license": "MIT" 3765 + }, 3766 + "node_modules/lazystream/node_modules/string_decoder": { 3767 + "version": "1.1.1", 3768 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 3769 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 3770 + "license": "MIT", 3771 + "dependencies": { 3772 + "safe-buffer": "~5.1.0" 3773 + } 3774 + }, 3397 3775 "node_modules/lib0": { 3398 3776 "version": "0.2.117", 3399 3777 "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", ··· 3439 3817 "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", 3440 3818 "license": "MIT" 3441 3819 }, 3820 + "node_modules/listenercount": { 3821 + "version": "1.0.1", 3822 + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", 3823 + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", 3824 + "license": "ISC" 3825 + }, 3826 + "node_modules/lodash.defaults": { 3827 + "version": "4.2.0", 3828 + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 3829 + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", 3830 + "license": "MIT" 3831 + }, 3832 + "node_modules/lodash.difference": { 3833 + "version": "4.5.0", 3834 + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", 3835 + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", 3836 + "license": "MIT" 3837 + }, 3838 + "node_modules/lodash.escaperegexp": { 3839 + "version": "4.1.2", 3840 + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", 3841 + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", 3842 + "license": "MIT" 3843 + }, 3844 + "node_modules/lodash.flatten": { 3845 + "version": "4.4.0", 3846 + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", 3847 + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", 3848 + "license": "MIT" 3849 + }, 3850 + "node_modules/lodash.groupby": { 3851 + "version": "4.6.0", 3852 + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", 3853 + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", 3854 + "license": "MIT" 3855 + }, 3856 + "node_modules/lodash.isboolean": { 3857 + "version": "3.0.3", 3858 + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 3859 + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", 3860 + "license": "MIT" 3861 + }, 3862 + "node_modules/lodash.isequal": { 3863 + "version": "4.5.0", 3864 + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", 3865 + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", 3866 + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", 3867 + "license": "MIT" 3868 + }, 3869 + "node_modules/lodash.isfunction": { 3870 + "version": "3.0.9", 3871 + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", 3872 + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", 3873 + "license": "MIT" 3874 + }, 3875 + "node_modules/lodash.isnil": { 3876 + "version": "4.0.0", 3877 + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", 3878 + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", 3879 + "license": "MIT" 3880 + }, 3881 + "node_modules/lodash.isplainobject": { 3882 + "version": "4.0.6", 3883 + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 3884 + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", 3885 + "license": "MIT" 3886 + }, 3887 + "node_modules/lodash.isundefined": { 3888 + "version": "3.0.1", 3889 + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", 3890 + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", 3891 + "license": "MIT" 3892 + }, 3893 + "node_modules/lodash.union": { 3894 + "version": "4.6.0", 3895 + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", 3896 + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", 3897 + "license": "MIT" 3898 + }, 3899 + "node_modules/lodash.uniq": { 3900 + "version": "4.5.0", 3901 + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", 3902 + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", 3903 + "license": "MIT" 3904 + }, 3442 3905 "node_modules/lop": { 3443 3906 "version": "0.4.2", 3444 3907 "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", ··· 3623 4086 "url": "https://github.com/sponsors/sindresorhus" 3624 4087 } 3625 4088 }, 4089 + "node_modules/minimatch": { 4090 + "version": "3.1.5", 4091 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", 4092 + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", 4093 + "license": "ISC", 4094 + "dependencies": { 4095 + "brace-expansion": "^1.1.7" 4096 + }, 4097 + "engines": { 4098 + "node": "*" 4099 + } 4100 + }, 3626 4101 "node_modules/minimist": { 3627 4102 "version": "1.2.8", 3628 4103 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", ··· 3630 4105 "license": "MIT", 3631 4106 "funding": { 3632 4107 "url": "https://github.com/sponsors/ljharb" 4108 + } 4109 + }, 4110 + "node_modules/mkdirp": { 4111 + "version": "0.5.6", 4112 + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 4113 + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 4114 + "license": "MIT", 4115 + "dependencies": { 4116 + "minimist": "^1.2.6" 4117 + }, 4118 + "bin": { 4119 + "mkdirp": "bin/cmd.js" 3633 4120 } 3634 4121 }, 3635 4122 "node_modules/mkdirp-classic": { ··· 3688 4175 }, 3689 4176 "engines": { 3690 4177 "node": ">=10" 4178 + } 4179 + }, 4180 + "node_modules/normalize-path": { 4181 + "version": "3.0.0", 4182 + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 4183 + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 4184 + "license": "MIT", 4185 + "engines": { 4186 + "node": ">=0.10.0" 3691 4187 } 3692 4188 }, 3693 4189 "node_modules/object-inspect": { ··· 4226 4722 "node": ">= 6" 4227 4723 } 4228 4724 }, 4725 + "node_modules/readdir-glob": { 4726 + "version": "1.1.3", 4727 + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", 4728 + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", 4729 + "license": "Apache-2.0", 4730 + "dependencies": { 4731 + "minimatch": "^5.1.0" 4732 + } 4733 + }, 4734 + "node_modules/readdir-glob/node_modules/brace-expansion": { 4735 + "version": "2.0.2", 4736 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 4737 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 4738 + "license": "MIT", 4739 + "dependencies": { 4740 + "balanced-match": "^1.0.0" 4741 + } 4742 + }, 4743 + "node_modules/readdir-glob/node_modules/minimatch": { 4744 + "version": "5.1.9", 4745 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", 4746 + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", 4747 + "license": "ISC", 4748 + "dependencies": { 4749 + "brace-expansion": "^2.0.1" 4750 + }, 4751 + "engines": { 4752 + "node": ">=10" 4753 + } 4754 + }, 4229 4755 "node_modules/regenerator-runtime": { 4230 4756 "version": "0.13.11", 4231 4757 "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", ··· 4271 4797 "optional": true, 4272 4798 "engines": { 4273 4799 "node": ">= 0.8.15" 4800 + } 4801 + }, 4802 + "node_modules/rimraf": { 4803 + "version": "2.7.1", 4804 + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 4805 + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 4806 + "deprecated": "Rimraf versions prior to v4 are no longer supported", 4807 + "license": "ISC", 4808 + "dependencies": { 4809 + "glob": "^7.1.3" 4810 + }, 4811 + "bin": { 4812 + "rimraf": "bin.js" 4274 4813 } 4275 4814 }, 4276 4815 "node_modules/rollup": { ··· 4595 5134 "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", 4596 5135 "license": "BSD-3-Clause" 4597 5136 }, 4598 - "node_modules/ssf": { 4599 - "version": "0.11.2", 4600 - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", 4601 - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", 4602 - "license": "Apache-2.0", 4603 - "dependencies": { 4604 - "frac": "~1.1.2" 4605 - }, 4606 - "engines": { 4607 - "node": ">=0.8" 4608 - } 4609 - }, 4610 5137 "node_modules/stackback": { 4611 5138 "version": "0.0.2", 4612 5139 "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", ··· 4820 5347 "dev": true, 4821 5348 "license": "MIT" 4822 5349 }, 5350 + "node_modules/tmp": { 5351 + "version": "0.2.5", 5352 + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", 5353 + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", 5354 + "license": "MIT", 5355 + "engines": { 5356 + "node": ">=14.14" 5357 + } 5358 + }, 4823 5359 "node_modules/toidentifier": { 4824 5360 "version": "1.0.1", 4825 5361 "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", ··· 4853 5389 }, 4854 5390 "engines": { 4855 5391 "node": ">=20" 5392 + } 5393 + }, 5394 + "node_modules/traverse": { 5395 + "version": "0.3.9", 5396 + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", 5397 + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", 5398 + "license": "MIT/X11", 5399 + "engines": { 5400 + "node": "*" 4856 5401 } 4857 5402 }, 4858 5403 "node_modules/tree-kill": { ··· 5468 6013 "node": ">= 0.8" 5469 6014 } 5470 6015 }, 6016 + "node_modules/unzipper": { 6017 + "version": "0.10.14", 6018 + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", 6019 + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", 6020 + "license": "MIT", 6021 + "dependencies": { 6022 + "big-integer": "^1.6.17", 6023 + "binary": "~0.3.0", 6024 + "bluebird": "~3.4.1", 6025 + "buffer-indexof-polyfill": "~1.0.0", 6026 + "duplexer2": "~0.1.4", 6027 + "fstream": "^1.0.12", 6028 + "graceful-fs": "^4.2.2", 6029 + "listenercount": "~1.0.1", 6030 + "readable-stream": "~2.3.6", 6031 + "setimmediate": "~1.0.4" 6032 + } 6033 + }, 6034 + "node_modules/unzipper/node_modules/readable-stream": { 6035 + "version": "2.3.8", 6036 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 6037 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 6038 + "license": "MIT", 6039 + "dependencies": { 6040 + "core-util-is": "~1.0.0", 6041 + "inherits": "~2.0.3", 6042 + "isarray": "~1.0.0", 6043 + "process-nextick-args": "~2.0.0", 6044 + "safe-buffer": "~5.1.1", 6045 + "string_decoder": "~1.1.1", 6046 + "util-deprecate": "~1.0.1" 6047 + } 6048 + }, 6049 + "node_modules/unzipper/node_modules/safe-buffer": { 6050 + "version": "5.1.2", 6051 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 6052 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 6053 + "license": "MIT" 6054 + }, 6055 + "node_modules/unzipper/node_modules/string_decoder": { 6056 + "version": "1.1.1", 6057 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 6058 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 6059 + "license": "MIT", 6060 + "dependencies": { 6061 + "safe-buffer": "~5.1.0" 6062 + } 6063 + }, 5471 6064 "node_modules/util-deprecate": { 5472 6065 "version": "1.0.2", 5473 6066 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", ··· 5490 6083 "license": "MIT", 5491 6084 "dependencies": { 5492 6085 "base64-arraybuffer": "^1.0.2" 6086 + } 6087 + }, 6088 + "node_modules/uuid": { 6089 + "version": "8.3.2", 6090 + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 6091 + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 6092 + "license": "MIT", 6093 + "bin": { 6094 + "uuid": "dist/bin/uuid" 5493 6095 } 5494 6096 }, 5495 6097 "node_modules/vary": { ··· 5730 6332 "node": ">=8" 5731 6333 } 5732 6334 }, 5733 - "node_modules/wmf": { 5734 - "version": "1.0.2", 5735 - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", 5736 - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", 5737 - "license": "Apache-2.0", 5738 - "engines": { 5739 - "node": ">=0.8" 5740 - } 5741 - }, 5742 - "node_modules/word": { 5743 - "version": "0.3.0", 5744 - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", 5745 - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", 5746 - "license": "Apache-2.0", 5747 - "engines": { 5748 - "node": ">=0.8" 5749 - } 5750 - }, 5751 6335 "node_modules/wrap-ansi": { 5752 6336 "version": "7.0.0", 5753 6337 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", ··· 5793 6377 } 5794 6378 } 5795 6379 }, 5796 - "node_modules/xlsx": { 5797 - "version": "0.18.5", 5798 - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", 5799 - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", 5800 - "license": "Apache-2.0", 5801 - "dependencies": { 5802 - "adler-32": "~1.3.0", 5803 - "cfb": "~1.2.1", 5804 - "codepage": "~1.15.0", 5805 - "crc-32": "~1.2.1", 5806 - "ssf": "~0.11.2", 5807 - "wmf": "~1.0.1", 5808 - "word": "~0.3.0" 5809 - }, 5810 - "bin": { 5811 - "xlsx": "bin/xlsx.njs" 5812 - }, 5813 - "engines": { 5814 - "node": ">=0.8" 5815 - } 5816 - }, 5817 6380 "node_modules/xml-name-validator": { 5818 6381 "version": "5.0.0", 5819 6382 "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", ··· 5837 6400 "version": "2.2.0", 5838 6401 "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", 5839 6402 "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", 5840 - "dev": true, 5841 6403 "license": "MIT" 5842 6404 }, 5843 6405 "node_modules/y-prosemirror": { ··· 5941 6503 "funding": { 5942 6504 "type": "GitHub Sponsors ❤", 5943 6505 "url": "https://github.com/sponsors/dmonad" 6506 + } 6507 + }, 6508 + "node_modules/zip-stream": { 6509 + "version": "4.1.1", 6510 + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", 6511 + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", 6512 + "license": "MIT", 6513 + "dependencies": { 6514 + "archiver-utils": "^3.0.4", 6515 + "compress-commons": "^4.1.2", 6516 + "readable-stream": "^3.6.0" 6517 + }, 6518 + "engines": { 6519 + "node": ">= 10" 6520 + } 6521 + }, 6522 + "node_modules/zip-stream/node_modules/archiver-utils": { 6523 + "version": "3.0.4", 6524 + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", 6525 + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", 6526 + "license": "MIT", 6527 + "dependencies": { 6528 + "glob": "^7.2.3", 6529 + "graceful-fs": "^4.2.0", 6530 + "lazystream": "^1.0.0", 6531 + "lodash.defaults": "^4.2.0", 6532 + "lodash.difference": "^4.5.0", 6533 + "lodash.flatten": "^4.4.0", 6534 + "lodash.isplainobject": "^4.0.6", 6535 + "lodash.union": "^4.6.0", 6536 + "normalize-path": "^3.0.0", 6537 + "readable-stream": "^3.6.0" 6538 + }, 6539 + "engines": { 6540 + "node": ">= 10" 5944 6541 } 5945 6542 } 5946 6543 }
+1 -1
package.json
··· 38 38 "better-sqlite3": "^11.7.0", 39 39 "chart.js": "^4.5.1", 40 40 "compression": "^1.7.5", 41 + "exceljs": "^4.4.0", 41 42 "express": "^4.21.0", 42 43 "html2pdf.js": "^0.14.0", 43 44 "lib0": "^0.2.99", ··· 46 47 "turndown": "^7.2.2", 47 48 "turndown-plugin-gfm": "^1.0.2", 48 49 "ws": "^8.18.0", 49 - "xlsx": "^0.18.5", 50 50 "y-prosemirror": "^1.2.15", 51 51 "yjs": "^13.6.20" 52 52 },
+1 -1
src/sheets/main.ts
··· 1728 1728 if (!file) return; 1729 1729 const ext = file.name.split('.').pop().toLowerCase(); 1730 1730 1731 - // Handle .xlsx files via SheetJS 1731 + // Handle .xlsx files via ExcelJS 1732 1732 if (ext === 'xlsx' || ext === 'xls') { 1733 1733 importXlsx(file, { 1734 1734 ydoc,
+41 -87
src/sheets/xlsx-export.ts
··· 1 1 /** 2 2 * .xlsx Export module for Tools Sheets. 3 3 * 4 - * Uses SheetJS (xlsx) to build workbooks from the internal cell data format. 4 + * Uses ExcelJS to build workbooks from the internal cell data format. 5 5 * Maps internal style properties back to Excel style objects. 6 6 * 7 7 * Pure logic (except downloadXlsx which touches the DOM). 8 8 */ 9 9 10 - /** 11 - * Convert a 1-based column number to a spreadsheet letter. 12 - * @param {number} col 13 - * @returns {string} 14 - */ 15 - function colToLetter(col) { 16 - let s = ''; 17 - let n = col; 18 - while (n > 0) { 19 - n--; 20 - s = String.fromCharCode(65 + (n % 26)) + s; 21 - n = Math.floor(n / 26); 22 - } 23 - return s; 24 - } 10 + import ExcelJS from 'exceljs'; 25 11 26 12 /** 27 13 * Map our internal format identifier back to an Excel number format string. ··· 40 26 } 41 27 42 28 /** 43 - * Map our internal style object to a SheetJS cell style object. 29 + * Map our internal style object to an ExcelJS-compatible style applied to a cell. 44 30 * 45 31 * @param {Object} s - Internal style object { bold, italic, fontSize, color, bg, align, format, ... } 46 - * @returns {Object} SheetJS style object 32 + * @returns {Object} Object with font, fill, alignment properties for ExcelJS 47 33 */ 48 34 export function mapStyleToXlsx(s) { 49 35 if (!s) return {}; 50 - const style = {}; 36 + const result: Record<string, unknown> = {}; 51 37 52 38 // Font properties 53 - const font = {}; 39 + const font: Record<string, unknown> = {}; 54 40 let hasFont = false; 55 41 if (s.bold) { font.bold = true; hasFont = true; } 56 42 if (s.italic) { font.italic = true; hasFont = true; } 57 43 if (s.underline) { font.underline = true; hasFont = true; } 58 44 if (s.strikethrough) { font.strike = true; hasFont = true; } 59 - if (s.fontSize) { font.sz = s.fontSize; hasFont = true; } 45 + if (s.fontSize) { font.size = s.fontSize; hasFont = true; } 60 46 if (s.color) { 61 47 // Strip leading # and ensure 6 hex digits 62 48 const hex = s.color.replace(/^#/, ''); 63 - font.color = { rgb: hex.length === 6 ? hex : hex.padStart(6, '0') }; 49 + font.color = { argb: 'FF' + (hex.length === 6 ? hex : hex.padStart(6, '0')) }; 64 50 hasFont = true; 65 51 } 66 - if (hasFont) style.font = font; 52 + if (hasFont) result.font = font; 67 53 68 54 // Fill (background color) 69 55 if (s.bg) { 70 56 const hex = s.bg.replace(/^#/, ''); 71 - style.fill = { 72 - patternType: 'solid', 73 - fgColor: { rgb: hex.length === 6 ? hex : hex.padStart(6, '0') }, 57 + result.fill = { 58 + type: 'pattern', 59 + pattern: 'solid', 60 + fgColor: { argb: 'FF' + (hex.length === 6 ? hex : hex.padStart(6, '0')) }, 74 61 }; 75 62 } 76 63 77 64 // Alignment 78 - const alignment = {}; 65 + const alignment: Record<string, unknown> = {}; 79 66 let hasAlignment = false; 80 67 if (s.align && ['left', 'center', 'right'].includes(s.align)) { 81 68 alignment.horizontal = s.align; 82 69 hasAlignment = true; 83 70 } 84 71 if (s.verticalAlign) { 85 - if (s.verticalAlign === 'middle') alignment.vertical = 'center'; 72 + if (s.verticalAlign === 'middle') alignment.vertical = 'middle'; 86 73 else alignment.vertical = s.verticalAlign; 87 74 hasAlignment = true; 88 75 } ··· 90 77 alignment.wrapText = true; 91 78 hasAlignment = true; 92 79 } 93 - if (hasAlignment) style.alignment = alignment; 80 + if (hasAlignment) result.alignment = alignment; 94 81 95 - return style; 82 + return result; 96 83 } 97 84 98 85 /** ··· 104 91 * @param {number} colCount 105 92 * @param {function(number): number} colWidths - (col) => width in pixels 106 93 * @param {string} sheetName - Name for the worksheet 107 - * @param {object} XLSX - The SheetJS library object 108 - * @returns {ArrayBuffer} 94 + * @returns {Promise<ArrayBuffer>} 109 95 */ 110 - export function exportToXlsx(getCellData, rowCount, colCount, colWidths, sheetName, XLSX) { 111 - const workbook = XLSX.utils.book_new(); 112 - const worksheet = {}; 96 + export async function exportToXlsx(getCellData, rowCount, colCount, colWidths, sheetName) { 97 + const workbook = new ExcelJS.Workbook(); 98 + const worksheet = workbook.addWorksheet(sheetName || 'Sheet 1'); 113 99 114 - let maxRow = 0; 115 - let maxCol = 0; 100 + // Set column widths (ExcelJS width units are ~1 character wide, approx px/7) 101 + if (colWidths) { 102 + for (let c = 1; c <= Math.max(colCount, 1); c++) { 103 + const px = colWidths(c); 104 + worksheet.getColumn(c).width = Math.max(Math.round(px / 7), 8); 105 + } 106 + } 116 107 117 108 for (let r = 1; r <= rowCount; r++) { 118 109 for (let c = 1; c <= colCount; c++) { ··· 124 115 continue; 125 116 } 126 117 127 - const addr = colToLetter(c) + r; 128 - const cell = {}; 118 + const cell = worksheet.getCell(r, c); 129 119 130 - // Value and type 120 + // Value and formula 131 121 if (data.f) { 132 - cell.f = data.f; 133 - // SheetJS needs a type even for formula cells 134 - if (typeof data.v === 'number') { 135 - cell.t = 'n'; 136 - cell.v = data.v; 137 - } else { 138 - cell.t = 's'; 139 - cell.v = data.v !== undefined && data.v !== null && data.v !== '' ? data.v : ''; 140 - } 141 - } else if (typeof data.v === 'number') { 142 - cell.t = 'n'; 143 - cell.v = data.v; 144 - } else if (typeof data.v === 'boolean') { 145 - cell.t = 'b'; 146 - cell.v = data.v; 147 - } else { 148 - cell.t = 's'; 149 - cell.v = data.v !== undefined && data.v !== null ? String(data.v) : ''; 122 + cell.value = { formula: data.f, result: typeof data.v === 'number' ? data.v : (data.v || '') }; 123 + } else if (data.v !== undefined && data.v !== null && data.v !== '') { 124 + cell.value = data.v; 150 125 } 151 126 152 - // Style mapping 127 + // Styles 153 128 if (data.s && Object.keys(data.s).length > 0) { 154 - cell.s = mapStyleToXlsx(data.s); 129 + const mapped = mapStyleToXlsx(data.s); 130 + 131 + if (mapped.font) cell.font = mapped.font as Partial<ExcelJS.Font>; 132 + if (mapped.fill) cell.fill = mapped.fill as ExcelJS.Fill; 133 + if (mapped.alignment) cell.alignment = mapped.alignment as Partial<ExcelJS.Alignment>; 155 134 156 135 // Number format 157 136 const excelFormat = mapFormatToExcel(data.s.format); 158 137 if (excelFormat) { 159 - cell.z = excelFormat; 138 + cell.numFmt = excelFormat; 160 139 } 161 140 } 162 - 163 - worksheet[addr] = cell; 164 - 165 - if (r > maxRow) maxRow = r; 166 - if (c > maxCol) maxCol = c; 167 141 } 168 142 } 169 143 170 - // Set range 171 - if (maxRow > 0 && maxCol > 0) { 172 - worksheet['!ref'] = 'A1:' + colToLetter(maxCol) + maxRow; 173 - } else { 174 - worksheet['!ref'] = 'A1'; 175 - } 176 - 177 - // Column widths (SheetJS uses wch = width in characters, approximate from pixels) 178 - if (colWidths) { 179 - const cols = []; 180 - for (let c = 1; c <= Math.max(maxCol, 1); c++) { 181 - const px = colWidths(c); 182 - // Approximate: 1 character ~= 7px at default font size 183 - cols.push({ wch: Math.max(Math.round(px / 7), 8) }); 184 - } 185 - worksheet['!cols'] = cols; 186 - } 187 - 188 - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName || 'Sheet 1'); 189 - 190 - const buffer = XLSX.write(workbook, { type: 'array', bookType: 'xlsx', cellStyles: true }); 144 + const buffer = await workbook.xlsx.writeBuffer(); 191 145 return buffer; 192 146 } 193 147
+107 -113
src/sheets/xlsx-import.ts
··· 1 1 /** 2 2 * .xlsx Import module for Tools Sheets. 3 3 * 4 - * Uses SheetJS (xlsx) to parse Excel files and convert them to the 4 + * Uses ExcelJS to parse Excel files and convert them to the 5 5 * cell data format used by the Yjs-backed spreadsheet. 6 + * ExcelJS properly extracts cell styles (bold, colors, alignment, etc.) 7 + * which the free SheetJS version does not support. 6 8 */ 9 + 10 + import ExcelJS from 'exceljs'; 7 11 8 12 /** 9 13 * @typedef {Object} CellData ··· 49 53 } 50 54 51 55 /** 52 - * Parse a single worksheet from an xlsx workbook. 53 - * 54 - * @param {object} worksheet - SheetJS worksheet object 55 - * @param {string} sheetName - Name of the sheet 56 - * @param {object} XLSX - The SheetJS library object 57 - * @returns {ParsedSheet} 56 + * Extract style properties from an ExcelJS cell into our internal style format. 58 57 */ 59 - function parseWorksheet(worksheet, sheetName, XLSX) { 60 - const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1'); 58 + function extractStyle(cell) { 59 + const s: Record<string, unknown> = {}; 60 + const style = cell.style || {}; 61 61 62 - const cells = new Map(); 63 - let maxRow = 0; 64 - let maxCol = 0; 62 + // Font 63 + if (style.font) { 64 + if (style.font.bold) s.bold = true; 65 + if (style.font.italic) s.italic = true; 66 + if (style.font.underline) s.underline = true; 67 + if (style.font.strike) s.strikethrough = true; 68 + if (style.font.size) s.fontSize = style.font.size; 69 + if (style.font.color?.argb) { 70 + s.color = '#' + style.font.color.argb.slice(-6); 71 + } 72 + } 65 73 66 - for (let r = range.s.r; r <= range.e.r; r++) { 67 - for (let c = range.s.c; c <= range.e.c; c++) { 68 - const cellAddress = XLSX.utils.encode_cell({ r, c }); 69 - const cell = worksheet[cellAddress]; 70 - if (!cell) continue; 74 + // Fill (background color) 75 + if (style.fill) { 76 + if (style.fill.type === 'pattern' && style.fill.fgColor?.argb) { 77 + s.bg = '#' + style.fill.fgColor.argb.slice(-6); 78 + } 79 + } 71 80 72 - const row = r + 1; // Convert to 1-based 73 - const col = c + 1; 74 - const id = cellId(col, row); 81 + // Alignment 82 + if (style.alignment) { 83 + if (style.alignment.horizontal && ['left', 'center', 'right'].includes(style.alignment.horizontal)) { 84 + s.align = style.alignment.horizontal; 85 + } 86 + if (style.alignment.vertical) { 87 + const va = style.alignment.vertical; 88 + if (va === 'top') s.verticalAlign = 'top'; 89 + else if (va === 'middle') s.verticalAlign = 'middle'; 90 + else if (va === 'bottom') s.verticalAlign = 'bottom'; 91 + } 92 + if (style.alignment.wrapText) s.wrap = true; 93 + } 75 94 76 - const data = { v: '', f: '', s: {} }; 95 + // Number format 96 + if (style.numFmt && style.numFmt !== 'General') { 97 + const fmt = mapExcelFormat(style.numFmt); 98 + if (fmt) s.format = fmt; 99 + } 77 100 78 - // Extract value 79 - if (cell.v !== undefined && cell.v !== null) { 80 - data.v = cell.v; 81 - } 101 + return s; 102 + } 82 103 83 - // Extract formula (stored without leading =) 84 - if (cell.f) { 85 - data.f = cell.f; 86 - } 104 + /** 105 + * Parse ALL sheets from an .xlsx ArrayBuffer using ExcelJS. 106 + * 107 + * @param {ArrayBuffer} arrayBuffer - The .xlsx file contents 108 + * @returns {Promise<{ sheets: ParsedSheet[] }>} Object with array of parsed sheets 109 + */ 110 + export async function parseXlsxWorkbook(arrayBuffer) { 111 + const workbook = new ExcelJS.Workbook(); 112 + await workbook.xlsx.load(arrayBuffer); 87 113 88 - // Extract style hints 89 - if (cell.s) { 90 - if (cell.s.font && cell.s.font.bold) { 91 - data.s.bold = true; 92 - } 93 - if (cell.s.font && cell.s.font.italic) { 94 - data.s.italic = true; 95 - } 96 - if (cell.s.font && cell.s.font.underline) { 97 - data.s.underline = true; 98 - } 99 - if (cell.s.font && cell.s.font.strike) { 100 - data.s.strikethrough = true; 101 - } 102 - if (cell.s.font && cell.s.font.sz) { 103 - data.s.fontSize = cell.s.font.sz; 104 - } 105 - if (cell.s.font && cell.s.font.color && cell.s.font.color.rgb) { 106 - data.s.color = '#' + cell.s.font.color.rgb.slice(-6); 107 - } 108 - if (cell.s.fill && cell.s.fill.fgColor && cell.s.fill.fgColor.rgb) { 109 - data.s.bg = '#' + cell.s.fill.fgColor.rgb.slice(-6); 110 - } 111 - if (cell.s.alignment && cell.s.alignment.horizontal) { 112 - const align = cell.s.alignment.horizontal; 113 - if (['left', 'center', 'right'].includes(align)) { 114 - data.s.align = align; 114 + const sheets: Array<{ 115 + name: string; 116 + cells: Map<string, { v: unknown; f: string; s: Record<string, unknown> }>; 117 + rowCount: number; 118 + colCount: number; 119 + }> = []; 120 + 121 + workbook.eachSheet((worksheet) => { 122 + const cells = new Map<string, { v: unknown; f: string; s: Record<string, unknown> }>(); 123 + let maxRow = 0; 124 + let maxCol = 0; 125 + 126 + worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => { 127 + row.eachCell({ includeEmpty: false }, (cell, colNumber) => { 128 + const id = cellId(colNumber, rowNumber); 129 + const data: { v: unknown; f: string; s: Record<string, unknown> } = { v: '', f: '', s: {} }; 130 + 131 + // Value & formula 132 + if (cell.value !== null && cell.value !== undefined) { 133 + if (typeof cell.value === 'object' && cell.value !== null && 'formula' in cell.value) { 134 + // Formula cell — ExcelJS stores as { formula, result } 135 + const formulaValue = cell.value as { formula?: string; result?: unknown }; 136 + data.f = formulaValue.formula || ''; 137 + data.v = formulaValue.result ?? ''; 138 + } else { 139 + data.v = cell.value; 115 140 } 116 141 } 117 - if (cell.s.alignment && cell.s.alignment.vertical) { 118 - const va = cell.s.alignment.vertical; 119 - if (va === 'top') data.s.verticalAlign = 'top'; 120 - else if (va === 'center') data.s.verticalAlign = 'middle'; 121 - else if (va === 'bottom') data.s.verticalAlign = 'bottom'; 122 - } 123 - if (cell.s.alignment && cell.s.alignment.wrapText) { 124 - data.s.wrap = true; 142 + 143 + // cell.formula is also set by ExcelJS for formula cells 144 + if (cell.formula) { 145 + data.f = cell.formula; 125 146 } 126 - } 127 147 128 - // Extract number format 129 - if (cell.z && cell.z !== 'General') { 130 - data.s.format = mapExcelFormat(cell.z); 131 - } 148 + // Styles 149 + data.s = extractStyle(cell); 132 150 133 - cells.set(id, data); 151 + cells.set(id, data); 152 + if (rowNumber > maxRow) maxRow = rowNumber; 153 + if (colNumber > maxCol) maxCol = colNumber; 154 + }); 155 + }); 134 156 135 - if (row > maxRow) maxRow = row; 136 - if (col > maxCol) maxCol = col; 137 - } 138 - } 157 + sheets.push({ 158 + name: worksheet.name, 159 + cells, 160 + rowCount: maxRow, 161 + colCount: maxCol, 162 + }); 163 + }); 139 164 140 - return { 141 - name: sheetName, 142 - cells, 143 - rowCount: maxRow, 144 - colCount: maxCol, 145 - }; 165 + return { sheets }; 146 166 } 147 167 148 168 /** 149 - * Parse an .xlsx ArrayBuffer using the provided XLSX library. 169 + * Parse an .xlsx ArrayBuffer using ExcelJS. 150 170 * Returns the first sheet only (backwards-compatible). 151 171 * 152 172 * @param {ArrayBuffer} arrayBuffer - The .xlsx file contents 153 - * @param {object} XLSX - The SheetJS library object 154 - * @returns {ParsedSheet} Parsed sheet data 173 + * @returns {Promise<ParsedSheet>} Parsed sheet data 155 174 */ 156 - export function parseXlsxWithLib(arrayBuffer, XLSX) { 157 - const workbook = XLSX.read(arrayBuffer, { type: 'array', cellStyles: true, cellFormula: true }); 175 + export async function parseXlsxWithLib(arrayBuffer) { 176 + const { sheets } = await parseXlsxWorkbook(arrayBuffer); 158 177 159 - const sheetName = workbook.SheetNames[0]; 160 - if (!sheetName) { 178 + if (sheets.length === 0) { 161 179 return { name: 'Sheet 1', cells: new Map(), rowCount: 0, colCount: 0 }; 162 180 } 163 181 164 - const worksheet = workbook.Sheets[sheetName]; 165 - return parseWorksheet(worksheet, sheetName, XLSX); 166 - } 167 - 168 - /** 169 - * Parse ALL sheets from an .xlsx ArrayBuffer. 170 - * 171 - * @param {ArrayBuffer} arrayBuffer - The .xlsx file contents 172 - * @param {object} XLSX - The SheetJS library object 173 - * @returns {{ sheets: ParsedSheet[] }} Object with array of parsed sheets 174 - */ 175 - export function parseXlsxWorkbook(arrayBuffer, XLSX) { 176 - const workbook = XLSX.read(arrayBuffer, { type: 'array', cellStyles: true, cellFormula: true }); 177 - 178 - if (!workbook.SheetNames || workbook.SheetNames.length === 0) { 179 - return { sheets: [] }; 180 - } 181 - 182 - const sheets = workbook.SheetNames.map((name) => { 183 - const worksheet = workbook.Sheets[name]; 184 - return parseWorksheet(worksheet, name, XLSX); 185 - }); 186 - 187 - return { sheets }; 182 + return sheets[0]; 188 183 } 189 184 190 185 /** ··· 261 256 return; 262 257 } 263 258 264 - const XLSX = await import('xlsx'); 265 - const { sheets } = parseXlsxWorkbook(arrayBuffer, XLSX); 259 + const { sheets } = await parseXlsxWorkbook(arrayBuffer); 266 260 267 261 if (sheets.length === 0 || sheets.every(s => s.cells.size === 0)) { 268 262 showToast('The .xlsx file appears to be empty', 3000);
+90 -90
tests/xlsx-export.test.ts
··· 1 1 import { describe, it, expect } from 'vitest'; 2 2 import { exportToXlsx, mapStyleToXlsx } from '../src/sheets/xlsx-export.js'; 3 - import * as XLSX from 'xlsx'; 3 + import ExcelJS from 'exceljs'; 4 4 5 5 /** 6 6 * Helper: create a getCellData function from a 2D array of cell objects. ··· 17 17 } 18 18 19 19 /** 20 - * Helper: export a grid and read it back with SheetJS for round-trip testing. 20 + * Helper: export a grid and read it back with ExcelJS for round-trip testing. 21 21 */ 22 - function exportAndRead(cells, rowCount, colCount, opts = {}) { 22 + async function exportAndRead(cells, rowCount, colCount, opts: { colWidths?: (c: number) => number; sheetName?: string } = {}) { 23 23 const getCellData = gridFromCells(cells); 24 - const colWidths = opts.colWidths || ((c) => 96); 24 + const colWidths = opts.colWidths || (() => 96); 25 25 const sheetName = opts.sheetName || 'Sheet 1'; 26 - const buffer = exportToXlsx(getCellData, rowCount, colCount, colWidths, sheetName, XLSX); 27 - const workbook = XLSX.read(buffer, { type: 'array', cellStyles: true, cellFormula: true }); 26 + const buffer = await exportToXlsx(getCellData, rowCount, colCount, colWidths, sheetName); 27 + const workbook = new ExcelJS.Workbook(); 28 + await workbook.xlsx.load(buffer); 28 29 return workbook; 29 30 } 30 31 ··· 57 58 expect(style.font.strike).toBe(true); 58 59 }); 59 60 60 - it('maps fontSize to font.sz', () => { 61 + it('maps fontSize to font.size', () => { 61 62 const style = mapStyleToXlsx({ fontSize: 14 }); 62 - expect(style.font.sz).toBe(14); 63 + expect(style.font.size).toBe(14); 63 64 }); 64 65 65 - it('maps color to font.color.rgb (stripping #)', () => { 66 + it('maps color to font.color.argb (with FF prefix)', () => { 66 67 const style = mapStyleToXlsx({ color: '#FF0000' }); 67 - expect(style.font.color.rgb).toBe('FF0000'); 68 + expect(style.font.color.argb).toBe('FFFF0000'); 68 69 }); 69 70 70 - it('maps bg to fill.fgColor.rgb', () => { 71 + it('maps bg to fill.fgColor.argb', () => { 71 72 const style = mapStyleToXlsx({ bg: '#00FF00' }); 72 - expect(style.fill.fgColor.rgb).toBe('00FF00'); 73 - expect(style.fill.patternType).toBe('solid'); 73 + expect(style.fill.fgColor.argb).toBe('FF00FF00'); 74 + expect(style.fill.pattern).toBe('solid'); 74 75 }); 75 76 76 77 it('maps align to alignment.horizontal', () => { ··· 78 79 expect(style.alignment.horizontal).toBe('center'); 79 80 }); 80 81 81 - it('maps verticalAlign middle to alignment.vertical center', () => { 82 + it('maps verticalAlign middle to alignment.vertical middle', () => { 82 83 const style = mapStyleToXlsx({ verticalAlign: 'middle' }); 83 - expect(style.alignment.vertical).toBe('center'); 84 + expect(style.alignment.vertical).toBe('middle'); 84 85 }); 85 86 86 87 it('maps wrap to alignment.wrapText', () => { ··· 92 93 const style = mapStyleToXlsx({ bold: true, italic: true, color: '#0000FF', align: 'right' }); 93 94 expect(style.font.bold).toBe(true); 94 95 expect(style.font.italic).toBe(true); 95 - expect(style.font.color.rgb).toBe('0000FF'); 96 + expect(style.font.color.argb).toBe('FF0000FF'); 96 97 expect(style.alignment.horizontal).toBe('right'); 97 98 }); 98 99 }); ··· 101 102 // exportToXlsx — basic workbook structure 102 103 // ============================================================ 103 104 describe('XLSX Export — basic workbook', () => { 104 - it('creates a workbook with the correct sheet name', () => { 105 + it('creates a workbook with the correct sheet name', async () => { 105 106 const cells = [[{ v: 'test', f: '', s: {} }]]; 106 - const wb = exportAndRead(cells, 1, 1, { sheetName: 'MySheet' }); 107 - expect(wb.SheetNames[0]).toBe('MySheet'); 107 + const wb = await exportAndRead(cells, 1, 1, { sheetName: 'MySheet' }); 108 + expect(wb.worksheets[0].name).toBe('MySheet'); 108 109 }); 109 110 110 - it('exports string values', () => { 111 + it('exports string values', async () => { 111 112 const cells = [[{ v: 'hello', f: '', s: {} }]]; 112 - const wb = exportAndRead(cells, 1, 1); 113 - const ws = wb.Sheets[wb.SheetNames[0]]; 114 - expect(ws.A1.v).toBe('hello'); 113 + const wb = await exportAndRead(cells, 1, 1); 114 + const ws = wb.worksheets[0]; 115 + expect(ws.getCell('A1').value).toBe('hello'); 115 116 }); 116 117 117 - it('exports numeric values', () => { 118 + it('exports numeric values', async () => { 118 119 const cells = [[{ v: 42, f: '', s: {} }]]; 119 - const wb = exportAndRead(cells, 1, 1); 120 - const ws = wb.Sheets[wb.SheetNames[0]]; 121 - expect(ws.A1.v).toBe(42); 122 - expect(ws.A1.t).toBe('n'); 120 + const wb = await exportAndRead(cells, 1, 1); 121 + const ws = wb.worksheets[0]; 122 + expect(ws.getCell('A1').value).toBe(42); 123 123 }); 124 124 125 - it('exports multiple rows and columns', () => { 125 + it('exports multiple rows and columns', async () => { 126 126 const cells = [ 127 127 [{ v: 'A1', f: '', s: {} }, { v: 'B1', f: '', s: {} }], 128 128 [{ v: 'A2', f: '', s: {} }, { v: 'B2', f: '', s: {} }], 129 129 ]; 130 - const wb = exportAndRead(cells, 2, 2); 131 - const ws = wb.Sheets[wb.SheetNames[0]]; 132 - expect(ws.A1.v).toBe('A1'); 133 - expect(ws.B1.v).toBe('B1'); 134 - expect(ws.A2.v).toBe('A2'); 135 - expect(ws.B2.v).toBe('B2'); 130 + const wb = await exportAndRead(cells, 2, 2); 131 + const ws = wb.worksheets[0]; 132 + expect(ws.getCell('A1').value).toBe('A1'); 133 + expect(ws.getCell('B1').value).toBe('B1'); 134 + expect(ws.getCell('A2').value).toBe('A2'); 135 + expect(ws.getCell('B2').value).toBe('B2'); 136 136 }); 137 137 138 - it('skips null/empty cells', () => { 138 + it('skips null/empty cells', async () => { 139 139 const cells = [ 140 140 [{ v: 'A1', f: '', s: {} }, null], 141 141 [null, { v: 'B2', f: '', s: {} }], 142 142 ]; 143 - const wb = exportAndRead(cells, 2, 2); 144 - const ws = wb.Sheets[wb.SheetNames[0]]; 145 - expect(ws.A1.v).toBe('A1'); 146 - expect(ws.B2.v).toBe('B2'); 147 - // B1 and A2 should not be present (or be undefined) 148 - expect(ws.B1).toBeUndefined(); 149 - expect(ws.A2).toBeUndefined(); 143 + const wb = await exportAndRead(cells, 2, 2); 144 + const ws = wb.worksheets[0]; 145 + expect(ws.getCell('A1').value).toBe('A1'); 146 + expect(ws.getCell('B2').value).toBe('B2'); 147 + // B1 and A2 should be null/empty 148 + expect(ws.getCell('B1').value).toBeNull(); 149 + expect(ws.getCell('A2').value).toBeNull(); 150 150 }); 151 151 152 - it('returns an ArrayBuffer-like output', () => { 152 + it('returns an ArrayBuffer-like output', async () => { 153 153 const cells = [[{ v: 'x', f: '', s: {} }]]; 154 154 const getCellData = gridFromCells(cells); 155 - const buffer = exportToXlsx(getCellData, 1, 1, () => 96, 'Test', XLSX); 155 + const buffer = await exportToXlsx(getCellData, 1, 1, () => 96, 'Test'); 156 156 expect(buffer).toBeTruthy(); 157 157 expect(buffer.byteLength || buffer.length).toBeGreaterThan(0); 158 158 }); ··· 162 162 // exportToXlsx — formulas 163 163 // ============================================================ 164 164 describe('XLSX Export — formula preservation', () => { 165 - it('preserves formulas in cells', () => { 165 + it('preserves formulas in cells', async () => { 166 166 const cells = [[{ v: 10, f: 'SUM(A2:A5)', s: {} }]]; 167 167 const getCellData = gridFromCells(cells); 168 - const buffer = exportToXlsx(getCellData, 1, 1, () => 96, 'Sheet 1', XLSX); 169 - const wb = XLSX.read(buffer, { type: 'array', cellFormula: true }); 170 - const ws = wb.Sheets[wb.SheetNames[0]]; 171 - expect(ws.A1.f).toBe('SUM(A2:A5)'); 168 + const buffer = await exportToXlsx(getCellData, 1, 1, () => 96, 'Sheet 1'); 169 + const wb = new ExcelJS.Workbook(); 170 + await wb.xlsx.load(buffer); 171 + const ws = wb.worksheets[0]; 172 + const cellValue = ws.getCell('A1').value; 173 + // ExcelJS stores formula cells as { formula, result } 174 + expect(typeof cellValue === 'object' && cellValue !== null && 'formula' in cellValue).toBe(true); 175 + expect((cellValue as { formula: string }).formula).toBe('SUM(A2:A5)'); 172 176 }); 173 177 174 - it('preserves formulas with string results', () => { 178 + it('preserves formulas with string results', async () => { 175 179 const cells = [[{ v: 'hello world', f: 'CONCATENATE(A2,A3)', s: {} }]]; 176 180 const getCellData = gridFromCells(cells); 177 - const buffer = exportToXlsx(getCellData, 1, 1, () => 96, 'Sheet 1', XLSX); 178 - const wb = XLSX.read(buffer, { type: 'array', cellFormula: true }); 179 - const ws = wb.Sheets[wb.SheetNames[0]]; 180 - expect(ws.A1.f).toBe('CONCATENATE(A2,A3)'); 181 + const buffer = await exportToXlsx(getCellData, 1, 1, () => 96, 'Sheet 1'); 182 + const wb = new ExcelJS.Workbook(); 183 + await wb.xlsx.load(buffer); 184 + const ws = wb.worksheets[0]; 185 + const cellValue = ws.getCell('A1').value; 186 + expect(typeof cellValue === 'object' && cellValue !== null && 'formula' in cellValue).toBe(true); 187 + expect((cellValue as { formula: string }).formula).toBe('CONCATENATE(A2,A3)'); 181 188 }); 182 189 }); 183 190 ··· 185 192 // exportToXlsx — style mapping 186 193 // ============================================================ 187 194 describe('XLSX Export — style mapping in cells', () => { 188 - it('exports bold style', () => { 195 + it('exports bold style', async () => { 189 196 const cells = [[{ v: 'bold', f: '', s: { bold: true } }]]; 190 - const getCellData = gridFromCells(cells); 191 - const buffer = exportToXlsx(getCellData, 1, 1, () => 96, 'Sheet 1', XLSX); 192 - // Read back and check the cell has a style object with bold 193 - const wb = XLSX.read(buffer, { type: 'array', cellStyles: true }); 194 - const ws = wb.Sheets[wb.SheetNames[0]]; 195 - // SheetJS may or may not round-trip styles depending on version 196 - // At minimum, the cell should exist with value 197 - expect(ws.A1.v).toBe('bold'); 197 + const wb = await exportAndRead(cells, 1, 1); 198 + const ws = wb.worksheets[0]; 199 + expect(ws.getCell('A1').value).toBe('bold'); 200 + expect(ws.getCell('A1').font?.bold).toBe(true); 198 201 }); 199 202 200 - it('exports cells with background color style object', () => { 203 + it('exports cells with background color style', async () => { 201 204 const cells = [[{ v: 'colored', f: '', s: { bg: '#FF0000' } }]]; 202 - const getCellData = gridFromCells(cells); 203 - const buffer = exportToXlsx(getCellData, 1, 1, () => 96, 'Sheet 1', XLSX); 204 - // Verify buffer is valid xlsx 205 - const wb = XLSX.read(buffer, { type: 'array' }); 206 - expect(wb.SheetNames).toHaveLength(1); 207 - expect(wb.Sheets[wb.SheetNames[0]].A1.v).toBe('colored'); 205 + const wb = await exportAndRead(cells, 1, 1); 206 + const ws = wb.worksheets[0]; 207 + expect(ws.getCell('A1').value).toBe('colored'); 208 + // ExcelJS preserves fill styles in round-trip 209 + const fill = ws.getCell('A1').fill; 210 + expect(fill).toBeDefined(); 211 + if (fill && fill.type === 'pattern' && fill.fgColor) { 212 + expect(fill.fgColor.argb).toBe('FFFF0000'); 213 + } 208 214 }); 209 215 210 - it('exports number format for currency', () => { 216 + it('exports number format for currency', async () => { 211 217 const cells = [[{ v: 1234.56, f: '', s: { format: 'currency' } }]]; 212 - const getCellData = gridFromCells(cells); 213 - const buffer = exportToXlsx(getCellData, 1, 1, () => 96, 'Sheet 1', XLSX); 214 - const wb = XLSX.read(buffer, { type: 'array' }); 215 - const ws = wb.Sheets[wb.SheetNames[0]]; 216 - expect(ws.A1.v).toBe(1234.56); 218 + const wb = await exportAndRead(cells, 1, 1); 219 + const ws = wb.worksheets[0]; 220 + expect(ws.getCell('A1').value).toBe(1234.56); 217 221 }); 218 222 219 - it('exports column widths (set on worksheet before write)', () => { 220 - // SheetJS community edition may not round-trip !cols through write/read. 221 - // We verify the export function sets !cols on the worksheet by checking 222 - // the buffer is valid and contains data. 223 + it('exports column widths', async () => { 223 224 const cells = [[{ v: 'x', f: '', s: {} }, { v: 'y', f: '', s: {} }]]; 224 - const getCellData = gridFromCells(cells); 225 225 const colWidths = (c) => c === 1 ? 200 : 96; 226 - const buffer = exportToXlsx(getCellData, 1, 2, colWidths, 'Sheet 1', XLSX); 227 - // Verify the buffer is a valid xlsx that can be parsed 228 - const wb = XLSX.read(buffer, { type: 'array' }); 229 - expect(wb.SheetNames).toHaveLength(1); 230 - expect(wb.Sheets[wb.SheetNames[0]].A1.v).toBe('x'); 231 - expect(wb.Sheets[wb.SheetNames[0]].B1.v).toBe('y'); 226 + const wb = await exportAndRead(cells, 1, 2, { colWidths }); 227 + const ws = wb.worksheets[0]; 228 + expect(ws.getCell('A1').value).toBe('x'); 229 + expect(ws.getCell('B1').value).toBe('y'); 230 + // Column 1 should be wider than column 2 231 + expect(ws.getColumn(1).width).toBeGreaterThan(ws.getColumn(2).width); 232 232 }); 233 233 }); 234 234 ··· 248 248 249 249 it('handles color without hash prefix', () => { 250 250 const style = mapStyleToXlsx({ color: 'AABBCC' }); 251 - // Should still produce a valid color 252 - expect(style.font.color.rgb).toBe('AABBCC'); 251 + // Should still produce a valid color with FF prefix 252 + expect(style.font.color.argb).toBe('FFAABBCC'); 253 253 }); 254 254 });
+105 -163
tests/xlsx-import-styles.test.ts
··· 1 1 import { describe, it, expect } from 'vitest'; 2 2 import { parseXlsxWithLib } from '../src/sheets/xlsx-import.js'; 3 + import ExcelJS from 'exceljs'; 3 4 4 5 /** 5 - * Tests for expanded XLSX style import. 6 + * Tests for XLSX style import using ExcelJS. 6 7 * 7 - * Since SheetJS (xlsx) does not reliably round-trip cell styles through 8 - * aoa_to_sheet → write → read, we mock the XLSX library to inject 9 - * cell objects with known style properties. 8 + * Instead of mocking, we create actual ExcelJS workbooks with styled cells, 9 + * export them to buffers, then parse with our import function. 10 + * This tests the REAL library path end-to-end. 10 11 */ 11 12 12 13 /** 13 - * Create a mock XLSX library that returns a workbook with the given cells. 14 - * Each cell is { addr: 'A1', v: value, f: formula, s: styleObj, z: format }. 14 + * Helper: create an xlsx ArrayBuffer with a single cell that has the given style. 15 15 */ 16 - function mockXLSX(cells, sheetName = 'TestSheet', ref = 'A1:A1') { 17 - const worksheet = {}; 18 - for (const cell of cells) { 19 - const entry = {}; 20 - if (cell.v !== undefined) entry.v = cell.v; 21 - if (cell.t) entry.t = cell.t; 22 - if (cell.f) entry.f = cell.f; 23 - if (cell.s) entry.s = cell.s; 24 - if (cell.z) entry.z = cell.z; 25 - worksheet[cell.addr] = entry; 26 - } 27 - worksheet['!ref'] = ref; 28 - 29 - return { 30 - read: () => ({ 31 - SheetNames: [sheetName], 32 - Sheets: { [sheetName]: worksheet }, 33 - }), 34 - utils: { 35 - decode_range: (ref) => { 36 - // Simple A1:B2 parser 37 - const parts = ref.split(':'); 38 - const start = parseAddr(parts[0]); 39 - const end = parts[1] ? parseAddr(parts[1]) : start; 40 - return { s: start, e: end }; 41 - }, 42 - encode_cell: ({ r, c }) => { 43 - let s = ''; 44 - let n = c + 1; 45 - while (n > 0) { n--; s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26); } 46 - return s + (r + 1); 47 - }, 48 - }, 49 - }; 50 - } 51 - 52 - function parseAddr(addr) { 53 - const match = addr.match(/^([A-Z]+)(\d+)$/); 54 - if (!match) return { r: 0, c: 0 }; 55 - let c = 0; 56 - for (const ch of match[1]) { c = c * 26 + (ch.charCodeAt(0) - 64); } 57 - return { r: parseInt(match[2]) - 1, c: c - 1 }; 16 + async function createStyledXlsx(value: string | number, style: { 17 + font?: Partial<ExcelJS.Font>; 18 + fill?: ExcelJS.Fill; 19 + alignment?: Partial<ExcelJS.Alignment>; 20 + numFmt?: string; 21 + }) { 22 + const workbook = new ExcelJS.Workbook(); 23 + const worksheet = workbook.addWorksheet('TestSheet'); 24 + const cell = worksheet.getCell('A1'); 25 + cell.value = value; 26 + if (style.font) cell.font = style.font; 27 + if (style.fill) cell.fill = style.fill; 28 + if (style.alignment) cell.alignment = style.alignment; 29 + if (style.numFmt) cell.numFmt = style.numFmt; 30 + const buffer = await workbook.xlsx.writeBuffer(); 31 + return buffer; 58 32 } 59 33 60 34 // ============================================================ ··· 62 36 // ============================================================ 63 37 64 38 describe('XLSX import — italic', () => { 65 - it('extracts italic from cell.s.font.italic', () => { 66 - const XLSX = mockXLSX([ 67 - { addr: 'A1', v: 'test', s: { font: { italic: true } } }, 68 - ]); 69 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 39 + it('extracts italic from cell style', async () => { 40 + const buf = await createStyledXlsx('test', { font: { italic: true } }); 41 + const result = await parseXlsxWithLib(buf); 70 42 expect(result.cells.get('A1').s.italic).toBe(true); 71 43 }); 72 44 73 - it('does not set italic when font.italic is absent', () => { 74 - const XLSX = mockXLSX([ 75 - { addr: 'A1', v: 'test', s: { font: { bold: true } } }, 76 - ]); 77 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 45 + it('does not set italic when font.italic is absent', async () => { 46 + const buf = await createStyledXlsx('test', { font: { bold: true } }); 47 + const result = await parseXlsxWithLib(buf); 78 48 expect(result.cells.get('A1').s.italic).toBeUndefined(); 79 49 }); 80 50 }); ··· 84 54 // ============================================================ 85 55 86 56 describe('XLSX import — underline', () => { 87 - it('extracts underline from cell.s.font.underline', () => { 88 - const XLSX = mockXLSX([ 89 - { addr: 'A1', v: 'test', s: { font: { underline: true } } }, 90 - ]); 91 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 57 + it('extracts underline from cell style', async () => { 58 + const buf = await createStyledXlsx('test', { font: { underline: true } }); 59 + const result = await parseXlsxWithLib(buf); 92 60 expect(result.cells.get('A1').s.underline).toBe(true); 93 61 }); 94 62 }); ··· 98 66 // ============================================================ 99 67 100 68 describe('XLSX import — strikethrough', () => { 101 - it('extracts strikethrough from cell.s.font.strike', () => { 102 - const XLSX = mockXLSX([ 103 - { addr: 'A1', v: 'test', s: { font: { strike: true } } }, 104 - ]); 105 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 69 + it('extracts strikethrough from cell style', async () => { 70 + const buf = await createStyledXlsx('test', { font: { strike: true } }); 71 + const result = await parseXlsxWithLib(buf); 106 72 expect(result.cells.get('A1').s.strikethrough).toBe(true); 107 73 }); 108 74 }); ··· 112 78 // ============================================================ 113 79 114 80 describe('XLSX import — fontSize', () => { 115 - it('extracts font size from cell.s.font.sz', () => { 116 - const XLSX = mockXLSX([ 117 - { addr: 'A1', v: 'test', s: { font: { sz: 14 } } }, 118 - ]); 119 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 81 + it('extracts font size from cell style', async () => { 82 + const buf = await createStyledXlsx('test', { font: { size: 14 } }); 83 + const result = await parseXlsxWithLib(buf); 120 84 expect(result.cells.get('A1').s.fontSize).toBe(14); 121 85 }); 122 86 }); ··· 126 90 // ============================================================ 127 91 128 92 describe('XLSX import — text color', () => { 129 - it('extracts text color from cell.s.font.color.rgb', () => { 130 - const XLSX = mockXLSX([ 131 - { addr: 'A1', v: 'test', s: { font: { color: { rgb: 'FF0000' } } } }, 132 - ]); 133 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 93 + it('extracts text color from cell style', async () => { 94 + const buf = await createStyledXlsx('test', { font: { color: { argb: 'FFFF0000' } } }); 95 + const result = await parseXlsxWithLib(buf); 134 96 expect(result.cells.get('A1').s.color).toBe('#FF0000'); 135 97 }); 136 98 137 - it('handles ARGB format (strips alpha prefix)', () => { 138 - const XLSX = mockXLSX([ 139 - { addr: 'A1', v: 'test', s: { font: { color: { rgb: 'FF00FF00' } } } }, 140 - ]); 141 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 99 + it('handles ARGB format (strips alpha prefix)', async () => { 100 + const buf = await createStyledXlsx('test', { font: { color: { argb: 'FF00FF00' } } }); 101 + const result = await parseXlsxWithLib(buf); 142 102 expect(result.cells.get('A1').s.color).toBe('#00FF00'); 143 103 }); 144 104 }); ··· 148 108 // ============================================================ 149 109 150 110 describe('XLSX import — background color', () => { 151 - it('extracts bg from cell.s.fill.fgColor.rgb', () => { 152 - const XLSX = mockXLSX([ 153 - { addr: 'A1', v: 'test', s: { fill: { fgColor: { rgb: 'FFFF00' } } } }, 154 - ]); 155 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 111 + it('extracts bg from cell fill', async () => { 112 + const buf = await createStyledXlsx('test', { 113 + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF00' } }, 114 + }); 115 + const result = await parseXlsxWithLib(buf); 156 116 expect(result.cells.get('A1').s.bg).toBe('#FFFF00'); 157 117 }); 158 118 159 - it('handles ARGB format for background', () => { 160 - const XLSX = mockXLSX([ 161 - { addr: 'A1', v: 'test', s: { fill: { fgColor: { rgb: 'FF0000FF' } } } }, 162 - ]); 163 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 119 + it('handles ARGB format for background', async () => { 120 + const buf = await createStyledXlsx('test', { 121 + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF0000FF' } }, 122 + }); 123 + const result = await parseXlsxWithLib(buf); 164 124 expect(result.cells.get('A1').s.bg).toBe('#0000FF'); 165 125 }); 166 126 }); ··· 170 130 // ============================================================ 171 131 172 132 describe('XLSX import — horizontal alignment', () => { 173 - it('extracts left alignment', () => { 174 - const XLSX = mockXLSX([ 175 - { addr: 'A1', v: 'test', s: { alignment: { horizontal: 'left' } } }, 176 - ]); 177 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 133 + it('extracts left alignment', async () => { 134 + const buf = await createStyledXlsx('test', { alignment: { horizontal: 'left' } }); 135 + const result = await parseXlsxWithLib(buf); 178 136 expect(result.cells.get('A1').s.align).toBe('left'); 179 137 }); 180 138 181 - it('extracts center alignment', () => { 182 - const XLSX = mockXLSX([ 183 - { addr: 'A1', v: 'test', s: { alignment: { horizontal: 'center' } } }, 184 - ]); 185 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 139 + it('extracts center alignment', async () => { 140 + const buf = await createStyledXlsx('test', { alignment: { horizontal: 'center' } }); 141 + const result = await parseXlsxWithLib(buf); 186 142 expect(result.cells.get('A1').s.align).toBe('center'); 187 143 }); 188 144 189 - it('extracts right alignment', () => { 190 - const XLSX = mockXLSX([ 191 - { addr: 'A1', v: 'test', s: { alignment: { horizontal: 'right' } } }, 192 - ]); 193 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 145 + it('extracts right alignment', async () => { 146 + const buf = await createStyledXlsx('test', { alignment: { horizontal: 'right' } }); 147 + const result = await parseXlsxWithLib(buf); 194 148 expect(result.cells.get('A1').s.align).toBe('right'); 195 149 }); 196 150 197 - it('ignores unsupported alignment values', () => { 198 - const XLSX = mockXLSX([ 199 - { addr: 'A1', v: 'test', s: { alignment: { horizontal: 'justify' } } }, 200 - ]); 201 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 151 + it('ignores unsupported alignment values', async () => { 152 + const buf = await createStyledXlsx('test', { alignment: { horizontal: 'justify' as 'left' } }); 153 + const result = await parseXlsxWithLib(buf); 202 154 expect(result.cells.get('A1').s.align).toBeUndefined(); 203 155 }); 204 156 }); ··· 208 160 // ============================================================ 209 161 210 162 describe('XLSX import — vertical alignment', () => { 211 - it('extracts top vertical alignment', () => { 212 - const XLSX = mockXLSX([ 213 - { addr: 'A1', v: 'test', s: { alignment: { vertical: 'top' } } }, 214 - ]); 215 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 163 + it('extracts top vertical alignment', async () => { 164 + const buf = await createStyledXlsx('test', { alignment: { vertical: 'top' } }); 165 + const result = await parseXlsxWithLib(buf); 216 166 expect(result.cells.get('A1').s.verticalAlign).toBe('top'); 217 167 }); 218 168 219 - it('maps center vertical alignment to middle', () => { 220 - const XLSX = mockXLSX([ 221 - { addr: 'A1', v: 'test', s: { alignment: { vertical: 'center' } } }, 222 - ]); 223 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 169 + it('extracts middle vertical alignment', async () => { 170 + const buf = await createStyledXlsx('test', { alignment: { vertical: 'middle' } }); 171 + const result = await parseXlsxWithLib(buf); 224 172 expect(result.cells.get('A1').s.verticalAlign).toBe('middle'); 225 173 }); 226 174 227 - it('extracts bottom vertical alignment', () => { 228 - const XLSX = mockXLSX([ 229 - { addr: 'A1', v: 'test', s: { alignment: { vertical: 'bottom' } } }, 230 - ]); 231 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 175 + it('extracts bottom vertical alignment', async () => { 176 + const buf = await createStyledXlsx('test', { alignment: { vertical: 'bottom' } }); 177 + const result = await parseXlsxWithLib(buf); 232 178 expect(result.cells.get('A1').s.verticalAlign).toBe('bottom'); 233 179 }); 234 180 }); ··· 238 184 // ============================================================ 239 185 240 186 describe('XLSX import — wrap text', () => { 241 - it('extracts wrapText as wrap', () => { 242 - const XLSX = mockXLSX([ 243 - { addr: 'A1', v: 'test', s: { alignment: { wrapText: true } } }, 244 - ]); 245 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 187 + it('extracts wrapText as wrap', async () => { 188 + const buf = await createStyledXlsx('test', { alignment: { wrapText: true } }); 189 + const result = await parseXlsxWithLib(buf); 246 190 expect(result.cells.get('A1').s.wrap).toBe(true); 247 191 }); 248 192 }); ··· 252 196 // ============================================================ 253 197 254 198 describe('XLSX import — combined styles', () => { 255 - it('extracts all style properties from a fully styled cell', () => { 256 - const XLSX = mockXLSX([ 257 - { 258 - addr: 'A1', 259 - v: 'styled', 260 - s: { 261 - font: { 262 - bold: true, 263 - italic: true, 264 - underline: true, 265 - strike: true, 266 - sz: 16, 267 - color: { rgb: 'FF0000' }, 268 - }, 269 - fill: { 270 - fgColor: { rgb: 'FFFF00' }, 271 - }, 272 - alignment: { 273 - horizontal: 'center', 274 - vertical: 'bottom', 275 - wrapText: true, 276 - }, 277 - }, 199 + it('extracts all style properties from a fully styled cell', async () => { 200 + const buf = await createStyledXlsx('styled', { 201 + font: { 202 + bold: true, 203 + italic: true, 204 + underline: true, 205 + strike: true, 206 + size: 16, 207 + color: { argb: 'FFFF0000' }, 208 + }, 209 + fill: { 210 + type: 'pattern', 211 + pattern: 'solid', 212 + fgColor: { argb: 'FFFFFF00' }, 213 + }, 214 + alignment: { 215 + horizontal: 'center', 216 + vertical: 'bottom', 217 + wrapText: true, 278 218 }, 279 - ]); 280 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 219 + }); 220 + const result = await parseXlsxWithLib(buf); 281 221 const s = result.cells.get('A1').s; 282 222 expect(s.bold).toBe(true); 283 223 expect(s.italic).toBe(true); ··· 291 231 expect(s.wrap).toBe(true); 292 232 }); 293 233 294 - it('handles cell with no style object', () => { 295 - const XLSX = mockXLSX([ 296 - { addr: 'A1', v: 'plain' }, 297 - ]); 298 - const result = parseXlsxWithLib(new ArrayBuffer(0), XLSX); 234 + it('handles cell with no style object', async () => { 235 + const workbook = new ExcelJS.Workbook(); 236 + const worksheet = workbook.addWorksheet('TestSheet'); 237 + worksheet.getCell('A1').value = 'plain'; 238 + const buf = await workbook.xlsx.writeBuffer(); 239 + 240 + const result = await parseXlsxWithLib(buf); 299 241 expect(result.cells.get('A1').s).toBeDefined(); 300 242 expect(result.cells.get('A1').s.bold).toBeUndefined(); 301 243 expect(result.cells.get('A1').s.italic).toBeUndefined();
+61 -52
tests/xlsx-import.test.ts
··· 1 1 import { describe, it, expect } from 'vitest'; 2 2 import { parseXlsxWithLib, mapExcelFormat, isValidXlsx } from '../src/sheets/xlsx-import.js'; 3 - import * as XLSX from 'xlsx'; 3 + import ExcelJS from 'exceljs'; 4 4 5 5 /** 6 6 * Helper: create a minimal .xlsx ArrayBuffer from a 2D array of values. 7 - * Uses SheetJS to build a real workbook in memory. 7 + * Uses ExcelJS to build a real workbook in memory. 8 8 */ 9 - function createXlsx(data, sheetName = 'Sheet1', opts = {}) { 10 - const workbook = XLSX.utils.book_new(); 11 - const worksheet = XLSX.utils.aoa_to_sheet(data); 9 + async function createXlsx(data: (string | number | boolean | null)[][], sheetName = 'Sheet1', opts: { cellOverrides?: Record<string, { formula?: string }> } = {}) { 10 + const workbook = new ExcelJS.Workbook(); 11 + const worksheet = workbook.addWorksheet(sheetName); 12 + 13 + for (let r = 0; r < data.length; r++) { 14 + for (let c = 0; c < data[r].length; c++) { 15 + const val = data[r][c]; 16 + if (val !== null && val !== undefined) { 17 + worksheet.getCell(r + 1, c + 1).value = val; 18 + } 19 + } 20 + } 12 21 13 - // Apply optional cell-level overrides (formulas, styles, formats) 22 + // Apply optional cell-level overrides (formulas) 14 23 if (opts.cellOverrides) { 15 24 for (const [addr, overrides] of Object.entries(opts.cellOverrides)) { 16 - if (!worksheet[addr]) worksheet[addr] = { t: 's', v: '' }; 17 - Object.assign(worksheet[addr], overrides); 25 + const cell = worksheet.getCell(addr); 26 + if (overrides.formula) { 27 + cell.value = { formula: overrides.formula, result: cell.value as number ?? 0 }; 28 + } 18 29 } 19 30 } 20 31 21 - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); 22 - const buffer = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }); 23 - return buffer; 32 + return await workbook.xlsx.writeBuffer(); 24 33 } 25 34 26 35 describe('.xlsx Import — isValidXlsx', () => { 27 - it('returns true for a valid xlsx buffer', () => { 28 - const buf = createXlsx([['A']]); 36 + it('returns true for a valid xlsx buffer', async () => { 37 + const buf = await createXlsx([['A']]); 29 38 expect(isValidXlsx(buf)).toBe(true); 30 39 }); 31 40 ··· 50 59 }); 51 60 52 61 describe('.xlsx Import — parseXlsxWithLib', () => { 53 - it('parses a simple single-cell spreadsheet', () => { 54 - const buf = createXlsx([['Hello']]); 55 - const result = parseXlsxWithLib(buf, XLSX); 62 + it('parses a simple single-cell spreadsheet', async () => { 63 + const buf = await createXlsx([['Hello']]); 64 + const result = await parseXlsxWithLib(buf); 56 65 57 66 expect(result.name).toBe('Sheet1'); 58 67 expect(result.cells.size).toBe(1); ··· 61 70 expect(result.colCount).toBe(1); 62 71 }); 63 72 64 - it('parses multiple cells across rows and columns', () => { 65 - const buf = createXlsx([ 73 + it('parses multiple cells across rows and columns', async () => { 74 + const buf = await createXlsx([ 66 75 ['Name', 'Age', 'City'], 67 76 ['Alice', 30, 'NYC'], 68 77 ['Bob', 25, 'LA'], 69 78 ]); 70 - const result = parseXlsxWithLib(buf, XLSX); 79 + const result = await parseXlsxWithLib(buf); 71 80 72 81 expect(result.cells.size).toBe(9); 73 82 expect(result.cells.get('A1').v).toBe('Name'); ··· 77 86 expect(result.colCount).toBe(3); 78 87 }); 79 88 80 - it('preserves the sheet name', () => { 81 - const buf = createXlsx([['Data']], 'My Custom Sheet'); 82 - const result = parseXlsxWithLib(buf, XLSX); 89 + it('preserves the sheet name', async () => { 90 + const buf = await createXlsx([['Data']], 'My Custom Sheet'); 91 + const result = await parseXlsxWithLib(buf); 83 92 expect(result.name).toBe('My Custom Sheet'); 84 93 }); 85 94 86 - it('handles numeric values', () => { 87 - const buf = createXlsx([ 95 + it('handles numeric values', async () => { 96 + const buf = await createXlsx([ 88 97 [1, 2.5, -3, 0], 89 98 ]); 90 - const result = parseXlsxWithLib(buf, XLSX); 99 + const result = await parseXlsxWithLib(buf); 91 100 expect(result.cells.get('A1').v).toBe(1); 92 101 expect(result.cells.get('B1').v).toBe(2.5); 93 102 expect(result.cells.get('C1').v).toBe(-3); 94 103 expect(result.cells.get('D1').v).toBe(0); 95 104 }); 96 105 97 - it('handles empty cells (sparse data)', () => { 98 - const buf = createXlsx([ 106 + it('handles empty cells (sparse data)', async () => { 107 + const buf = await createXlsx([ 99 108 ['A', null, 'C'], 100 109 [null, 'B', null], 101 110 ]); 102 - const result = parseXlsxWithLib(buf, XLSX); 111 + const result = await parseXlsxWithLib(buf); 103 112 104 113 // Empty cells may or may not be in the map, but populated ones must be correct 105 114 expect(result.cells.get('A1').v).toBe('A'); ··· 109 118 expect(result.colCount).toBe(3); 110 119 }); 111 120 112 - it('extracts formulas as text', () => { 113 - const buf = createXlsx( 121 + it('extracts formulas as text', async () => { 122 + const buf = await createXlsx( 114 123 [ 115 124 [1, 2], 116 125 [3, null], ··· 118 127 'Sheet1', 119 128 { 120 129 cellOverrides: { 121 - 'B2': { t: 'n', v: 3, f: 'A1+A2' }, 130 + 'B2': { formula: 'A1+A2' }, 122 131 }, 123 132 } 124 133 ); 125 - const result = parseXlsxWithLib(buf, XLSX); 134 + const result = await parseXlsxWithLib(buf); 126 135 const cellB2 = result.cells.get('B2'); 127 136 expect(cellB2.f).toBe('A1+A2'); 128 137 }); 129 138 130 - it('handles an empty workbook', () => { 131 - const workbook = XLSX.utils.book_new(); 132 - const worksheet = XLSX.utils.aoa_to_sheet([]); 133 - XLSX.utils.book_append_sheet(workbook, worksheet, 'Empty'); 134 - const buf = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }); 139 + it('handles an empty workbook', async () => { 140 + const workbook = new ExcelJS.Workbook(); 141 + workbook.addWorksheet('Empty'); 142 + const buf = await workbook.xlsx.writeBuffer(); 135 143 136 - const result = parseXlsxWithLib(buf, XLSX); 144 + const result = await parseXlsxWithLib(buf); 145 + // Empty sheet has 0 populated cells 137 146 expect(result.cells.size).toBe(0); 138 147 }); 139 148 140 - it('handles a large number of cells', () => { 141 - const data = []; 149 + it('handles a large number of cells', async () => { 150 + const data: string[][] = []; 142 151 for (let r = 0; r < 50; r++) { 143 - const row = []; 152 + const row: string[] = []; 144 153 for (let c = 0; c < 10; c++) { 145 154 row.push(`R${r}C${c}`); 146 155 } 147 156 data.push(row); 148 157 } 149 - const buf = createXlsx(data); 150 - const result = parseXlsxWithLib(buf, XLSX); 158 + const buf = await createXlsx(data); 159 + const result = await parseXlsxWithLib(buf); 151 160 expect(result.cells.size).toBe(500); 152 161 expect(result.rowCount).toBe(50); 153 162 expect(result.colCount).toBe(10); ··· 155 164 expect(result.cells.get('J50').v).toBe('R49C9'); 156 165 }); 157 166 158 - it('handles mixed types (strings, numbers, booleans)', () => { 159 - const buf = createXlsx([ 167 + it('handles mixed types (strings, numbers, booleans)', async () => { 168 + const buf = await createXlsx([ 160 169 ['text', 42, true, false], 161 170 ]); 162 - const result = parseXlsxWithLib(buf, XLSX); 171 + const result = await parseXlsxWithLib(buf); 163 172 expect(result.cells.get('A1').v).toBe('text'); 164 173 expect(result.cells.get('B1').v).toBe(42); 165 174 expect(result.cells.get('C1').v).toBe(true); 166 175 expect(result.cells.get('D1').v).toBe(false); 167 176 }); 168 177 169 - it('initializes style object for each cell', () => { 170 - const buf = createXlsx([['test']]); 171 - const result = parseXlsxWithLib(buf, XLSX); 178 + it('initializes style object for each cell', async () => { 179 + const buf = await createXlsx([['test']]); 180 + const result = await parseXlsxWithLib(buf); 172 181 expect(result.cells.get('A1').s).toBeDefined(); 173 182 expect(typeof result.cells.get('A1').s).toBe('object'); 174 183 }); 175 184 176 - it('initializes formula as empty string when no formula', () => { 177 - const buf = createXlsx([['test']]); 178 - const result = parseXlsxWithLib(buf, XLSX); 185 + it('initializes formula as empty string when no formula', async () => { 186 + const buf = await createXlsx([['test']]); 187 + const result = await parseXlsxWithLib(buf); 179 188 expect(result.cells.get('A1').f).toBe(''); 180 189 }); 181 190 });
+53 -58
tests/xlsx-multi-sheet.test.ts
··· 1 1 import { describe, it, expect } from 'vitest'; 2 2 import { parseXlsxWorkbook, parseXlsxWithLib } from '../src/sheets/xlsx-import.js'; 3 - import * as XLSX from 'xlsx'; 3 + import ExcelJS from 'exceljs'; 4 4 5 5 /** 6 - * Helper: create an .xlsx ArrayBuffer with multiple sheets. 6 + * Helper: create an .xlsx ArrayBuffer with multiple sheets using ExcelJS. 7 7 * @param {Array<{ name: string, data: any[][] }>} sheets 8 - * @returns {ArrayBuffer} 8 + * @returns {Promise<ArrayBuffer>} 9 9 */ 10 - function createMultiSheetXlsx(sheets) { 11 - const workbook = XLSX.utils.book_new(); 10 + async function createMultiSheetXlsx(sheets: Array<{ name: string; data: (string | number | null)[][] }>) { 11 + const workbook = new ExcelJS.Workbook(); 12 12 for (const sheet of sheets) { 13 - const worksheet = XLSX.utils.aoa_to_sheet(sheet.data); 14 - XLSX.utils.book_append_sheet(workbook, worksheet, sheet.name); 13 + const worksheet = workbook.addWorksheet(sheet.name); 14 + for (let r = 0; r < sheet.data.length; r++) { 15 + for (let c = 0; c < sheet.data[r].length; c++) { 16 + const val = sheet.data[r][c]; 17 + if (val !== null && val !== undefined) { 18 + worksheet.getCell(r + 1, c + 1).value = val; 19 + } 20 + } 21 + } 15 22 } 16 - return XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }); 23 + return await workbook.xlsx.writeBuffer(); 17 24 } 18 25 19 26 /** 20 27 * Helper: create a single-sheet .xlsx ArrayBuffer. 21 28 */ 22 - function createXlsx(data, sheetName = 'Sheet1') { 29 + async function createXlsx(data: (string | number | null)[][], sheetName = 'Sheet1') { 23 30 return createMultiSheetXlsx([{ name: sheetName, data }]); 24 31 } 25 32 ··· 27 34 // parseXlsxWorkbook — multi-sheet parsing 28 35 // ============================================================ 29 36 describe('Multi-Sheet XLSX Import — parseXlsxWorkbook', () => { 30 - it('parses a 2-sheet workbook and returns both sheets', () => { 31 - const buf = createMultiSheetXlsx([ 37 + it('parses a 2-sheet workbook and returns both sheets', async () => { 38 + const buf = await createMultiSheetXlsx([ 32 39 { name: 'Sales', data: [['Product', 'Revenue'], ['Widget', 100]] }, 33 40 { name: 'Costs', data: [['Item', 'Cost'], ['Material', 50]] }, 34 41 ]); 35 - const result = parseXlsxWorkbook(buf, XLSX); 42 + const result = await parseXlsxWorkbook(buf); 36 43 expect(result.sheets).toHaveLength(2); 37 44 }); 38 45 39 - it('preserves sheet names', () => { 40 - const buf = createMultiSheetXlsx([ 46 + it('preserves sheet names', async () => { 47 + const buf = await createMultiSheetXlsx([ 41 48 { name: 'First Sheet', data: [['a']] }, 42 49 { name: 'Second Sheet', data: [['b']] }, 43 50 ]); 44 - const result = parseXlsxWorkbook(buf, XLSX); 51 + const result = await parseXlsxWorkbook(buf); 45 52 expect(result.sheets[0].name).toBe('First Sheet'); 46 53 expect(result.sheets[1].name).toBe('Second Sheet'); 47 54 }); 48 55 49 - it('parses data correctly in each sheet', () => { 50 - const buf = createMultiSheetXlsx([ 56 + it('parses data correctly in each sheet', async () => { 57 + const buf = await createMultiSheetXlsx([ 51 58 { name: 'Sheet1', data: [['Hello', 'World']] }, 52 59 { name: 'Sheet2', data: [['Foo'], ['Bar']] }, 53 60 ]); 54 - const result = parseXlsxWorkbook(buf, XLSX); 61 + const result = await parseXlsxWorkbook(buf); 55 62 56 63 // Sheet 1 57 64 expect(result.sheets[0].cells.get('A1').v).toBe('Hello'); ··· 66 73 expect(result.sheets[1].colCount).toBe(1); 67 74 }); 68 75 69 - it('handles a workbook with 3 sheets', () => { 70 - const buf = createMultiSheetXlsx([ 76 + it('handles a workbook with 3 sheets', async () => { 77 + const buf = await createMultiSheetXlsx([ 71 78 { name: 'A', data: [['a1']] }, 72 79 { name: 'B', data: [['b1']] }, 73 80 { name: 'C', data: [['c1']] }, 74 81 ]); 75 - const result = parseXlsxWorkbook(buf, XLSX); 82 + const result = await parseXlsxWorkbook(buf); 76 83 expect(result.sheets).toHaveLength(3); 77 84 expect(result.sheets[2].name).toBe('C'); 78 85 expect(result.sheets[2].cells.get('A1').v).toBe('c1'); 79 86 }); 80 87 81 - it('handles sheets with different dimensions', () => { 82 - const buf = createMultiSheetXlsx([ 88 + it('handles sheets with different dimensions', async () => { 89 + const buf = await createMultiSheetXlsx([ 83 90 { name: 'Small', data: [['x']] }, 84 91 { name: 'Big', data: [ 85 92 ['a', 'b', 'c'], ··· 87 94 ['g', 'h', 'i'], 88 95 ]}, 89 96 ]); 90 - const result = parseXlsxWorkbook(buf, XLSX); 97 + const result = await parseXlsxWorkbook(buf); 91 98 expect(result.sheets[0].rowCount).toBe(1); 92 99 expect(result.sheets[0].colCount).toBe(1); 93 100 expect(result.sheets[1].rowCount).toBe(3); 94 101 expect(result.sheets[1].colCount).toBe(3); 95 102 }); 96 103 97 - it('handles an empty workbook', () => { 98 - // Create a workbook with no sheets — SheetJS requires at least one, 99 - // so we use a mock for this edge case 100 - const mockXLSX = { 101 - read: () => ({ SheetNames: [], Sheets: {} }), 102 - utils: { 103 - decode_range: () => ({ s: { r: 0, c: 0 }, e: { r: 0, c: 0 } }), 104 - encode_cell: ({ r, c }) => { 105 - let s = ''; 106 - let n = c + 1; 107 - while (n > 0) { n--; s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26); } 108 - return s + (r + 1); 109 - }, 110 - }, 111 - }; 112 - const result = parseXlsxWorkbook(new ArrayBuffer(0), mockXLSX); 104 + it('handles an empty workbook', async () => { 105 + // Create a workbook with no data sheets 106 + const workbook = new ExcelJS.Workbook(); 107 + const buf = await workbook.xlsx.writeBuffer(); 108 + const result = await parseXlsxWorkbook(buf); 113 109 expect(result.sheets).toHaveLength(0); 114 110 }); 115 111 116 - it('handles a single-sheet workbook (backwards compat)', () => { 117 - const buf = createXlsx([['only']]); 118 - const result = parseXlsxWorkbook(buf, XLSX); 112 + it('handles a single-sheet workbook (backwards compat)', async () => { 113 + const buf = await createXlsx([['only']]); 114 + const result = await parseXlsxWorkbook(buf); 119 115 expect(result.sheets).toHaveLength(1); 120 116 expect(result.sheets[0].name).toBe('Sheet1'); 121 117 expect(result.sheets[0].cells.get('A1').v).toBe('only'); 122 118 }); 123 119 124 - it('preserves numeric values across sheets', () => { 125 - const buf = createMultiSheetXlsx([ 120 + it('preserves numeric values across sheets', async () => { 121 + const buf = await createMultiSheetXlsx([ 126 122 { name: 'Numbers', data: [[42, 3.14]] }, 127 123 { name: 'More', data: [[100]] }, 128 124 ]); 129 - const result = parseXlsxWorkbook(buf, XLSX); 125 + const result = await parseXlsxWorkbook(buf); 130 126 expect(result.sheets[0].cells.get('A1').v).toBe(42); 131 127 expect(result.sheets[0].cells.get('B1').v).toBe(3.14); 132 128 expect(result.sheets[1].cells.get('A1').v).toBe(100); 133 129 }); 134 130 135 - it('handles empty sheets within a multi-sheet workbook', () => { 131 + it('handles empty sheets within a multi-sheet workbook', async () => { 136 132 // One sheet has data, the other is empty 137 - const workbook = XLSX.utils.book_new(); 138 - const ws1 = XLSX.utils.aoa_to_sheet([['data']]); 139 - XLSX.utils.book_append_sheet(workbook, ws1, 'HasData'); 140 - const ws2 = XLSX.utils.aoa_to_sheet([]); 141 - XLSX.utils.book_append_sheet(workbook, ws2, 'Empty'); 142 - const buf = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }); 133 + const workbook = new ExcelJS.Workbook(); 134 + const ws1 = workbook.addWorksheet('HasData'); 135 + ws1.getCell('A1').value = 'data'; 136 + workbook.addWorksheet('Empty'); 137 + const buf = await workbook.xlsx.writeBuffer(); 143 138 144 - const result = parseXlsxWorkbook(buf, XLSX); 139 + const result = await parseXlsxWorkbook(buf); 145 140 expect(result.sheets).toHaveLength(2); 146 141 expect(result.sheets[0].cells.size).toBeGreaterThan(0); 147 - // Empty sheet may have 0 cells or a minimal cell 142 + // Empty sheet should have 0 cells 148 143 expect(result.sheets[1].name).toBe('Empty'); 149 144 }); 150 145 }); ··· 153 148 // Backwards compatibility — parseXlsxWithLib still works 154 149 // ============================================================ 155 150 describe('Multi-Sheet XLSX Import — backwards compatibility', () => { 156 - it('parseXlsxWithLib still returns only the first sheet', () => { 157 - const buf = createMultiSheetXlsx([ 151 + it('parseXlsxWithLib still returns only the first sheet', async () => { 152 + const buf = await createMultiSheetXlsx([ 158 153 { name: 'First', data: [['first']] }, 159 154 { name: 'Second', data: [['second']] }, 160 155 ]); 161 - const result = parseXlsxWithLib(buf, XLSX); 156 + const result = await parseXlsxWithLib(buf); 162 157 expect(result.name).toBe('First'); 163 158 expect(result.cells.get('A1').v).toBe('first'); 164 159 // Should not include second sheet data