{"id":47928268,"url":"https://github.com/productdevbook/hucre","last_synced_at":"2026-05-29T00:00:52.383Z","repository":{"id":347363050,"uuid":"1190903403","full_name":"productdevbook/hucre","owner":"productdevbook","description":"Zero-dependency spreadsheet engine. Read \u0026 write XLSX, CSV, ODS. Pure TypeScript, works everywhere.","archived":false,"fork":false,"pushed_at":"2026-05-28T22:10:21.000Z","size":4187,"stargazers_count":1369,"open_issues_count":0,"forks_count":32,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T23:22:48.460Z","etag":null,"topics":["csv","csv-parser","esm","excel","ods","ods-parser","parser","spreadsheet","streaming","tree-shakeable","typescript","xlsx","xlsx-parser","xlsx-writer","zero-dependency"],"latest_commit_sha":null,"homepage":"https://hucre.productdevbook.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/productdevbook.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["productdevbook"]}},"created_at":"2026-03-24T18:22:34.000Z","updated_at":"2026-05-28T22:10:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/productdevbook/hucre","commit_stats":null,"previous_names":["productdevbook/hucre"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/productdevbook/hucre","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fhucre","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fhucre/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fhucre/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fhucre/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/productdevbook","download_url":"https://codeload.github.com/productdevbook/hucre/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fhucre/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33630999,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csv","csv-parser","esm","excel","ods","ods-parser","parser","spreadsheet","streaming","tree-shakeable","typescript","xlsx","xlsx-parser","xlsx-writer","zero-dependency"],"created_at":"2026-04-04T07:02:23.882Z","updated_at":"2026-05-29T00:00:52.375Z","avatar_url":"https://github.com/productdevbook.png","language":"TypeScript","funding_links":["https://github.com/sponsors/productdevbook"],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\".github/assets/cover.svg\" alt=\"hucre — Zero-dependency spreadsheet engine\" width=\"100%\"\u003e\n  \u003cbr\u003e\u003cbr\u003e\n  \u003cb style=\"font-size: 2em;\"\u003ehucre\u003c/b\u003e\n  \u003cbr\u003e\u003cbr\u003e\n  Zero-dependency spreadsheet engine.\n  \u003cbr\u003e\n  Read \u0026 write XLSX, CSV, ODS, JSON, NDJSON, XML. Schema validation, streaming, round-trip preservation. Pure TypeScript, works everywhere.\n  \u003cbr\u003e\u003cbr\u003e\n  \u003ca href=\"https://npmjs.com/package/hucre\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/hucre?style=flat\u0026colorA=18181B\u0026colorB=34d399\" alt=\"npm version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://npmjs.com/package/hucre\"\u003e\u003cimg src=\"https://img.shields.io/npm/dm/hucre?style=flat\u0026colorA=18181B\u0026colorB=34d399\" alt=\"npm downloads\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://bundlephobia.com/result?p=hucre\"\u003e\u003cimg src=\"https://img.shields.io/bundlephobia/minzip/hucre?style=flat\u0026colorA=18181B\u0026colorB=34d399\" alt=\"bundle size\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/productdevbook/hucre/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/productdevbook/hucre?style=flat\u0026colorA=18181B\u0026colorB=34d399\" alt=\"license\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n## Quick Start\n\n```sh\nnpm install hucre\n```\n\n```ts\nimport { readXlsx, writeXlsx } from \"hucre\"\n\n// Read an XLSX file\nconst workbook = await readXlsx(buffer)\nconsole.log(workbook.sheets[0].rows)\n\n// Write an XLSX file\nconst xlsx = await writeXlsx({\n  sheets: [\n    {\n      name: \"Products\",\n      columns: [\n        { header: \"Name\", key: \"name\", width: 25 },\n        { header: \"Price\", key: \"price\", width: 12, numFmt: \"$#,##0.00\" },\n        { header: \"Stock\", key: \"stock\", width: 10 },\n      ],\n      data: [\n        { name: \"Widget\", price: 9.99, stock: 142 },\n        { name: \"Gadget\", price: 24.5, stock: 87 },\n      ],\n    },\n  ],\n})\n```\n\n## Tree Shaking\n\nImport only what you need:\n\n```ts\nimport { readXlsx, writeXlsx } from \"hucre/xlsx\" // XLSX only\nimport { parseCsv, writeCsv } from \"hucre/csv\" // CSV only (~2 KB gzipped)\nimport { readOds, writeOds } from \"hucre/ods\" // ODS only\nimport { parseJson, writeNdjson } from \"hucre/json\" // JSON / NDJSON\nimport { readXml, writeXml } from \"hucre/xml\" // Tabular XML\n```\n\n## Why hucre?\n\n### vs JavaScript / TypeScript Libraries\n\n|                         | hucre     | SheetJS CE    | ExcelJS   | xlsx-js-style |\n| ----------------------- | --------- | ------------- | --------- | ------------- |\n| **Dependencies**        | 0         | 0\\*           | 12 (CVEs) | 0\\*           |\n| **Bundle (gzip)**       | ~18 KB    | ~300 KB       | ~500 KB   | ~300 KB       |\n| **ESM native**          | Yes       | Partial       | No (CJS)  | Partial       |\n| **TypeScript**          | Native    | Bolted-on     | Bolted-on | Bolted-on     |\n| **Edge runtime**        | Yes       | No            | No        | No            |\n| **CSP compliant**       | Yes       | Yes           | No (eval) | Yes           |\n| **npm published**       | Yes       | No (CDN only) | Stale     | Yes           |\n| **Read + Write**        | Yes       | Yes (Pro $)   | Yes       | Yes           |\n| **Styling**             | Yes       | No (Pro $)    | Yes       | Yes           |\n| **Cond. formatting**    | Yes (all) | No (Pro $)    | Partial   | No            |\n| **Stream read + write** | Yes       | CSV only      | Yes       | CSV only      |\n| **ODS support**         | Yes       | Yes           | No        | Yes           |\n| **Round-trip**          | Yes       | Partial       | Partial   | Partial       |\n| **Sparklines**          | Yes       | No            | No        | No            |\n| **Tables**              | Yes       | Yes           | Yes       | Yes           |\n| **Images**              | Yes       | No (Pro $)    | Yes       | No            |\n\n\\* SheetJS removed itself from npm; must install from CDN tarball.\n\n### vs Libraries in Other Languages\n\n|                       | hucre (TS)   | openpyxl (Py) | XlsxWriter (Py) | rust_xlsxwriter | Apache POI (Java) |\n| --------------------- | ------------ | ------------- | --------------- | --------------- | ----------------- |\n| **Read XLSX**         | Yes          | Yes           | No              | No              | Yes               |\n| **Write XLSX**        | Yes          | Yes           | Yes             | Yes             | Yes               |\n| **Streaming**         | Read+Write   | Write-only    | No              | const_memory    | SXSSF (write)     |\n| **Charts**            | Round-trip   | 15+ types     | 9 types         | 12+ types       | Limited           |\n| **Pivot tables**      | Read + Write | Read-only     | No              | No              | Limited           |\n| **Cond. formatting**  | Yes (all)    | Yes           | Yes             | Yes             | Yes               |\n| **Sparklines**        | Yes          | No            | Yes             | Yes             | No                |\n| **Formula eval**      | No           | No            | No              | No              | Yes               |\n| **Multi-format**      | XLSX/ODS/CSV | XLSX only     | XLSX only       | XLSX only       | XLS/XLSX          |\n| **Zero dependencies** | Yes          | lxml optional | No              | Yes             | No                |\n\n## Features\n\n### Reading\n\n```ts\nimport { readXlsx } from \"hucre/xlsx\"\n\nconst wb = await readXlsx(uint8Array, {\n  sheets: [0, \"Products\"], // Filter sheets by index or name\n  readStyles: true, // Parse cell styles\n  dateSystem: \"auto\", // Auto-detect 1900/1904\n})\n\nfor (const sheet of wb.sheets) {\n  console.log(sheet.name) // \"Products\"\n  console.log(sheet.rows) // CellValue[][]\n  console.log(sheet.merges) // MergeRange[]\n}\n```\n\n`sheets` also accepts a predicate that runs against lightweight metadata\n**before** each worksheet body is parsed — useful for visibility-based\nselection without paying the I/O cost of the full read:\n\n```ts\nconst wb = await readXlsx(buf, {\n  sheets: (info) =\u003e !info.hidden \u0026\u0026 !info.veryHidden,\n})\n// info: { name, index, hidden?, veryHidden? }\n```\n\nSupported cell types: strings, numbers, booleans, dates, formulas, rich text, errors, inline strings.\n\n### Writing\n\n```ts\nimport { writeXlsx } from \"hucre/xlsx\"\n\nconst buffer = await writeXlsx({\n  sheets: [\n    {\n      name: \"Report\",\n      columns: [\n        { header: \"Date\", key: \"date\", width: 15, numFmt: \"yyyy-mm-dd\" },\n        { header: \"Revenue\", key: \"revenue\", width: 15, numFmt: \"$#,##0.00\" },\n        { header: \"Active\", key: \"active\", width: 10 },\n      ],\n      data: [\n        { date: new Date(\"2026-01-15\"), revenue: 12500, active: true },\n        { date: new Date(\"2026-01-16\"), revenue: 8900, active: false },\n      ],\n      freezePane: { rows: 1 },\n      autoFilter: { range: \"A1:C3\" },\n    },\n  ],\n  defaultFont: { name: \"Calibri\", size: 11 },\n})\n```\n\nFeatures: cell styles, auto column widths, merged cells, freeze/split panes, auto-filter with criteria, data validation, hyperlinks, images (PNG/JPEG/GIF/SVG/WebP), comments, tables, conditional formatting (cellIs/colorScale/dataBar/iconSet), named ranges, print settings, page breaks, sheet protection, workbook protection, rich text, shared/array/dynamic formulas, sparklines, textboxes, background images, number formats, hidden sheets, Excel 2024 native checkboxes, HTML/Markdown/JSON/TSV export, template engine.\n\n### Auto Column Width\n\n```ts\nconst buffer = await writeXlsx({\n  sheets: [\n    {\n      name: \"Products\",\n      columns: [\n        { header: \"Name\", key: \"name\", autoWidth: true },\n        { header: \"Price\", key: \"price\", autoWidth: true, numFmt: \"$#,##0.00\" },\n        { header: \"SKU\", key: \"sku\", autoWidth: true },\n      ],\n      data: products,\n    },\n  ],\n})\n```\n\nCalculates optimal column widths from cell content — font-aware, handles CJK double-width characters, number formats, min/max constraints.\n\n### Data Validation\n\n```ts\nconst buffer = await writeXlsx({\n  sheets: [\n    {\n      name: \"Sheet1\",\n      rows: [\n        [\"Status\", \"Quantity\"],\n        [\"active\", 10],\n      ],\n      dataValidations: [\n        {\n          type: \"list\",\n          values: [\"active\", \"inactive\", \"draft\"],\n          range: \"A2:A100\",\n          showErrorMessage: true,\n          errorTitle: \"Invalid\",\n          errorMessage: \"Pick from the list\",\n        },\n        {\n          type: \"whole\",\n          operator: \"between\",\n          formula1: \"0\",\n          formula2: \"1000\",\n          range: \"B2:B100\",\n        },\n      ],\n    },\n  ],\n})\n```\n\n### Hyperlinks\n\n```ts\nconst buffer = await writeXlsx({\n  sheets: [\n    {\n      name: \"Links\",\n      rows: [[\"Visit Google\", \"Go to Sheet2\"]],\n      cells: new Map([\n        [\n          \"0,0\",\n          {\n            value: \"Visit Google\",\n            type: \"string\",\n            hyperlink: { target: \"https://google.com\", tooltip: \"Open Google\" },\n          },\n        ],\n        [\n          \"0,1\",\n          {\n            value: \"Go to Sheet2\",\n            type: \"string\",\n            hyperlink: { target: \"\", location: \"Sheet2!A1\" },\n          },\n        ],\n      ]),\n    },\n  ],\n})\n```\n\nFor tabular reports, put links **inline in `data` rows** instead of a parallel\n`cells` map — keyed by the column's `key`. Use the `link()` helper (or a plain\n`{ text, hyperlink, tooltip? }` object). A `#`-prefixed target is treated as an\ninternal reference (`#Sheet2!A1`).\n\n```ts\nimport { writeXlsx, link } from \"hucre/xlsx\"\n\nawait writeXlsx({\n  sheets: [\n    {\n      name: \"Summary\",\n      columns: [\n        { header: \"Link\", key: \"link\" },\n        { header: \"ID\", key: \"id\" },\n      ],\n      data: [\n        { link: link(\"Open\", \"https://example.com/items/abc-123\"), id: \"abc-123\" },\n        { link: { text: \"Open\", hyperlink: \"https://example.com/items/def-456\" }, id: \"def-456\" },\n      ],\n    },\n  ],\n})\n```\n\n### Streaming\n\nProcess large files row-by-row without loading everything into memory:\n\n```ts\nimport { streamXlsxRows, XlsxStreamWriter } from \"hucre/xlsx\"\n\n// Stream read — async generator yields rows one at a time\nfor await (const row of streamXlsxRows(buffer)) {\n  console.log(row.index, row.values)\n}\n\n// True streaming from a ReadableStream — the ZIP is parsed front-to-back\n// from local file headers, so the whole archive is never buffered. Only\n// the small metadata parts (content types, rels, workbook, shared strings,\n// styles) are read up front; the target worksheet is piped straight into\n// the SAX parser. (Falls back to buffering only for archives whose layout\n// rules out single-pass streaming — e.g. ZIP data descriptors, or shared\n// strings stored after the worksheet.)\nconst res = await fetch(\"https://example.com/huge.xlsx\")\nfor await (const row of streamXlsxRows(res.body!)) {\n  console.log(row.index, row.values)\n}\n\n// Cap the number of rows yielded (preview / sampling). The underlying\n// ZIP/SAX stream is cancelled once the cap is reached, so very large\n// sheets stay cheap.\nfor await (const row of streamXlsxRows(buffer, { maxRows: 100 })) {\n  console.log(row.index, row.values)\n}\n\n// Filter to an A1 range. Rows outside the row span are skipped; cells\n// outside the column span are masked to `null` (column indexes stay\n// stable). Parsing stops once a row past the end-row is observed.\nfor await (const row of streamXlsxRows(buffer, { range: \"B2:D1000\" })) {\n  // row.values[0] === null (column A is outside)\n  // row.values[1..3] carry B/C/D\n}\n\n// Stream write — add rows incrementally\nconst writer = new XlsxStreamWriter({\n  name: \"BigData\",\n  columns: [{ header: \"ID\" }, { header: \"Value\" }],\n  freezePane: { rows: 1 },\n})\nfor (let i = 0; i \u003c 100_000; i++) {\n  writer.addRow([i + 1, Math.random()])\n}\nconst buffer = await writer.finish()\n```\n\n#### Auto-split past Excel's row limit\n\nPass `maxRowsPerSheet` to spill into `{name}_2`, `{name}_3`, … when the\ndata crosses Excel's 1,048,576-row hard limit (default). The captured\nheader row is repeated on every rolled sheet.\n\n```ts\nimport { XlsxStreamWriter, XLSX_MAX_ROWS_PER_SHEET } from \"hucre/xlsx\"\n\nconst writer = new XlsxStreamWriter({\n  name: \"BigData\",\n  columns: [\n    { key: \"id\", header: \"ID\" },\n    { key: \"v\", header: \"Value\" },\n  ],\n  maxRowsPerSheet: 1_000_000, // optional override; default = 1_048_576\n  repeatHeaders: true, // default\n})\n\nfor (let i = 0; i \u003c 3_000_000; i++) writer.addRow([i + 1, Math.random()])\n// → BigData, BigData_2, BigData_3\nconst buf = await writer.finish()\n```\n\n### Password Protection\n\nRead and write password-protected XLSX workbooks (ECMA-376 **Agile**\nencryption — the Excel 2010+ scheme). Encryption is built on the\nplatform's WebCrypto, so it stays zero-dependency and runs in Node, Deno,\nBun, Cloudflare Workers, and browsers.\n\n```ts\nimport { writeXlsx, readXlsx, readObjects, EncryptedFileError, DecryptionError } from \"hucre\"\n\n// Write an encrypted workbook\nconst encrypted = await writeXlsx({\n  sheets: [{ name: \"Secret\", rows: [[\"pin\", 1234]] }],\n  encryption: { password: \"hunter2\" },\n})\n\n// Read it back with the password\nconst wb = await readXlsx(encrypted, { password: \"hunter2\" })\n\n// Works through every read entry point: read(), readObjects(), streamXlsxRows()\nconst rows = await readObjects(encrypted, { password: \"hunter2\" })\n\n// Without a password an encrypted file throws EncryptedFileError;\n// with the wrong password it throws DecryptionError.\ntry {\n  await readXlsx(encrypted)\n} catch (e) {\n  if (e instanceof EncryptedFileError) console.log(\"needs a password\")\n}\n```\n\nThe roundtrip path takes the same option: `await saveXlsx(wb, { encryption: { password } })`.\nThe key-derivation iteration count defaults to Excel's 100000; lower it via\n`encryption: { password, spinCount }` when the speed/security trade-off calls for it.\n\n### XLSB (Binary Excel) — read\n\nRead `.xlsb` (Excel Binary Workbook) files — the binary package format\nthat's smaller and faster to open than `.xlsx`. `read()` auto-detects it,\nor call `readXlsb` directly:\n\n```ts\nimport { read, readXlsb } from \"hucre\"\n\nconst wb = await readXlsb(bytes) // sheet names + typed cell values\nconst same = await read(bytes) // auto-detected (XLSX vs XLSB vs ODS)\n```\n\nDecodes shared strings, RK / floating-point numbers, inline strings,\nbooleans, error codes, cached formula values, and dates (via the binary\nstyle table). Read-only; password-protected `.xlsb` also decrypts with\n`{ password }`.\n\n### XLS (Legacy Excel 97-2003) — read\n\nRead legacy `.xls` (BIFF8) files — the OLE2/CFB binary format from Excel\n97-2003. `read()` auto-detects it, or call `readXls`:\n\n```ts\nimport { read, readXls } from \"hucre\"\n\nconst wb = await readXls(bytes)\nconst same = await read(bytes) // auto-detected\n```\n\nDecodes the shared-string table (with CONTINUE spanning), RK / MULRK /\nnumber / boolean / error cells, labels, cached formula values, dates, and\nmerged cells. Read-only.\n\n### ODS (OpenDocument)\n\n```ts\nimport { readOds, writeOds } from \"hucre/ods\"\n\nconst wb = await readOds(buffer)\nconst ods = await writeOds({ sheets: [{ name: \"Sheet1\", rows: [[\"Hello\", 42]] }] })\n```\n\n### Round-trip Preservation\n\nOpen, modify, save — without losing charts, macros, or features hucre doesn't natively handle:\n\n```ts\nimport { openXlsx, saveXlsx } from \"hucre/xlsx\"\n\nconst workbook = await openXlsx(buffer)\nworkbook.sheets[0].rows[0][0] = \"Updated!\"\nconst output = await saveXlsx(workbook) // Charts, VBA, themes preserved\n```\n\n### External Workbook References\n\n`[N]Sheet!Ref` references to other workbooks are read into a typed\n`workbook.externalLinks` model and re-declared on roundtrip — without\nthis the `\u003cexternalReferences\u003e` block and the matching relationship\ndisappear from `xl/workbook.xml.rels`, leaving Excel with orphan\n`externalLinkN.xml` parts that it ignores.\n\n```ts\nimport { readXlsx, parseExternalLink } from \"hucre\"\n\nconst wb = await readXlsx(buf)\nfor (const link of wb.externalLinks ?? []) {\n  console.log(link.target, link.targetMode, link.sheetNames)\n  for (const sheet of link.sheetData) {\n    for (const cell of sheet.cells) {\n      // cell.type ∈ \"n\" | \"s\" | \"b\" | \"e\" | \"str\"\n      console.log(cell.ref, cell.type, cell.value)\n    }\n  }\n}\n\n// Standalone parser when you already have the XML strings\nconst link = parseExternalLink(externalLinkXml, externalLinkRelsXml)\n```\n\nThe 1-based index in `workbook.externalLinks` matches the `[N]` prefix\nused by formulas like `[1]Sheet1!A1`. Cached `t=\"s\"` values stay as\nshared-string indices into the _external_ workbook (which hucre cannot\ndereference); resolved strings live in the linked file.\n\n### Cell-Embedded Images (WPS DISPIMG)\n\nWPS Office (and recent Excel versions) embed images inside cells via a\nworkbook-level `xl/cellimages.xml` registry referenced from\n`=_xlfn.DISPIMG(\"\u003cid\u003e\", 1)` formulas. hucre reads the registry into a\ntyped `workbook.cellImages` array and re-declares the part on\n`saveXlsx` so the DISPIMG link survives round-trips — without this the\nrelationship and content-type override are dropped and the formula\nloses its target.\n\n```ts\nimport { readXlsx } from \"hucre\"\n\nconst wb = await readXlsx(buf)\nfor (const img of wb.cellImages ?? []) {\n  console.log(img.id, img.type, img.description, img.data.byteLength)\n}\n\n// Standalone parsers when you already have the XML strings.\nimport { parseCellImages, assembleCellImages, REL_CELL_IMAGES } from \"hucre\"\nconst refs = parseCellImages(cellImagesXml)\nconst images = assembleCellImages(refs, mediaMap)\n```\n\nSynthesizing a `cellimages.xml` from a model on a fresh `writeXlsx`\ncall (without an existing source file) is a follow-up — for now the\nread + roundtrip-preserve side is in place.\n\n### Slicers \u0026 Timeline Filters\n\nSlicers (Excel 2010+) and timeline slicers (Excel 2013+) are read into\ntyped `workbook.slicerCaches` / `workbook.timelineCaches` plus per-sheet\n`sheet.slicers` / `sheet.timelines` arrays. On `saveXlsx` the slicer /\ntimeline parts are re-declared in `[Content_Types].xml`, the workbook\nrels, the workbook `extLst`, and each sheet's rels — without this\nroundtrip Excel saw the cache parts as orphans and dropped the\nslicers / timelines on next open.\n\n```ts\nimport { readXlsx } from \"hucre\"\n\nconst wb = await readXlsx(buf)\n\n// Workbook-level cache definitions.\nconsole.log(wb.slicerCaches) // SlicerCache[] (pivot-table or table source)\nconsole.log(wb.timelineCaches) // TimelineCache[]\n\n// Per-sheet slicer / timeline instances.\nfor (const sheet of wb.sheets) {\n  for (const s of sheet.slicers ?? []) console.log(s.name, s.cache, s.caption)\n  for (const t of sheet.timelines ?? []) console.log(t.name, t.cache, t.level)\n}\n\n// Standalone parsers when you already have the XML strings.\nimport { parseSlicers, parseSlicerCache, parseTimelines, parseTimelineCache } from \"hucre\"\n```\n\nThe worksheet body's `\u003cx14:slicerList\u003e` / `\u003cx15:timelines\u003e` extension\nblocks are not yet re-injected when the worksheet XML is regenerated —\nExcel still sees the parts as wired up via rels and content-types so\nthey survive the roundtrip, but synthesizing slicers from a fresh\nwrite is a follow-up.\n\n### Pivot Tables\n\nPivot tables (`xl/pivotTables/pivotTableN.xml`) and their workbook-level\ncache definitions (`xl/pivotCache/pivotCacheDefinitionN.xml` plus the\ncompanion `pivotCacheRecordsN.xml`) are read into typed\n`workbook.pivotCaches` and per-sheet `sheet.pivotTables` arrays. On\n`saveXlsx` the pivot parts are re-declared in `[Content_Types].xml`,\nthe workbook rels, the workbook `\u003cpivotCaches\u003e` block, and each host\nsheet's rels — Excel previously saw the pivot parts as orphans and\ndropped the tables on next open.\n\n```ts\nimport { readXlsx } from \"hucre\"\n\nconst wb = await readXlsx(buf)\n\n// Workbook-level cache definitions.\nfor (const cache of wb.pivotCaches ?? []) {\n  console.log(cache.cacheId, cache.sourceSheet, cache.sourceRef, cache.fieldNames)\n}\n\n// Per-sheet pivot table instances.\nfor (const sheet of wb.sheets) {\n  for (const pt of sheet.pivotTables ?? []) {\n    console.log(pt.name, pt.location, pt.cacheId)\n    for (const f of pt.fields) {\n      console.log(\"  \", f.name, f.axis, f.function)\n    }\n  }\n}\n\n// Standalone parsers when you already have the XML strings.\nimport { parsePivotTable, parsePivotCacheDefinition, attachPivotCacheFields } from \"hucre\"\n```\n\n`PivotTable.cacheId` matches the workbook-level `cacheId` rather than a\nper-table relationship, so reordering `Workbook.pivotCaches` keeps the\nlinks sound.\n\n`writeXlsx` can also author pivot tables from scratch via the per-sheet\n`pivotTables` field. Hucre emits the pivot cache (definition + cached\nrecords), the pivot layout, and every required relationship and content\ntype. The numeric layout (row totals, grand totals, value cells) is left\nfor Excel to compute on first open via the existing `fullCalcOnLoad`\nrecompute — Phase 1 ships the structural skeleton, not pre-computed\nvalue cells.\n\n```ts\nimport { writeXlsx } from \"hucre\"\n\nconst xlsx = await writeXlsx({\n  sheets: [\n    {\n      name: \"Data\",\n      rows: [\n        [\"Region\", \"Product\", \"Revenue\"],\n        [\"EU\", \"A\", 100],\n        [\"EU\", \"B\", 50],\n        [\"US\", \"A\", 200],\n        [\"US\", \"B\", 75],\n      ],\n    },\n    {\n      name: \"Pivot\",\n      pivotTables: [\n        {\n          name: \"SalesPivot\",\n          sourceSheet: \"Data\",\n          rows: [\"Region\"],\n          columns: [\"Product\"],\n          values: [{ field: \"Revenue\", function: \"sum\" }],\n        },\n      ],\n    },\n  ],\n})\n```\n\nSupported aggregation functions: `sum` (default), `count`, `average`,\n`max`, `min`, `product`, `countNums`, `stdDev`, `stdDevp`, `var`,\n`varp`. Pivots can source from their own sheet (omit `sourceSheet`)\nor any sibling sheet, and accept either `rows` (raw 2-D arrays) or\n`columns` + `data` (object-style) source shapes.\n\n### Charts\n\nCharts (`xl/charts/chartN.xml` plus the optional `styleN.xml` /\n`colorsN.xml` companions) round-trip through three layers:\n\n- `parseChart(xml)` / `readXlsx().sheets[].charts[]` — surfaces a\n  read-side `Chart` record (kinds, title, series, axes, legend, data\n  labels, fills / borders, manual layout, view3D, etc.).\n- `writeXlsx({ sheets: [{ charts: [SheetChart] }] })` — emits the\n  chart parts and re-anchors the drawing in the regenerated worksheet\n  body so Excel never sees the chart as an orphan.\n- `cloneChart(source, options)` — bridges the two: it converts a\n  parsed `Chart` into a writable `SheetChart`, applies a typed override\n  bag (`undefined` = inherit, `null` = drop, value = replace), and\n  returns the result ready for `writeXlsx`.\n\n`getCharts(workbook)` flattens every chart anchored on the workbook's\nsheets into a single array; `addChart(sheet, chart)` is the symmetric\nwriter-side helper that appends a `SheetChart` to a `WriteSheet`.\n\n#### Capability matrix\n\nThe table summarises which knobs flow through each layer. \"Read\"\nmeans `parseChart` surfaces the field on `Chart` (or\n`ChartAxisInfo` / `ChartDataLabelsInfo` / `ChartDataTable` for nested\nslots); \"Write\" means `writeXlsx` emits it from the matching\n`SheetChart` field; \"Clone\" means `cloneChart` carries it through\nwith the standard `undefined` / `null` / value override grammar.\n\n| Family                                                                                                                       | Read | Write | Clone |\n| ---------------------------------------------------------------------------------------------------------------------------- | :--: | :---: | :---: |\n| Chart kinds (bar / column / line / pie / doughnut / area / scatter / bubble / combo)                                         |  x   |   x   |   x   |\n| Title text + visibility (`title`, `showTitle`, `autoTitleDeleted`)                                                           |  x   |   x   |   x   |\n| Title typography (size / bold / italic / strike / underline / color / family)                                                |  x   |   x   |   x   |\n| Title rotation, manual layout                                                                                                |  x   |   x   |   x   |\n| Title fill / border color / border width / border dash                                                                       |  x   |   x   |   x   |\n| Legend position, overlay, font knobs, manual layout                                                                          |  x   |   x   |   x   |\n| Legend fill / border color / border width / border dash, per-entry hide                                                      |  x   |   x   |   x   |\n| Plot-area manual layout, fill / border color / border width / border dash                                                    |  x   |   x   |   x   |\n| Chart-space fill / border color / border width / border dash, rounded corners, style preset                                  |  x   |   x   |   x   |\n| Axis title text + typography (per-axis), rotation, manual layout, fill / border knobs                                        |  x   |   x   |   x   |\n| Axis label rotation, font size, bold / italic / underline / strike, color, font family                                       |  x   |   x   |   x   |\n| Axis scale (min / max / logBase / majorUnit / minorUnit), reverse, hidden, crosses, dispUnits                                |  x   |   x   |   x   |\n| Axis number format, tick marks, tick-label position / skip, label offset / alignment                                         |  x   |   x   |   x   |\n| Axis gridlines (major / minor)                                                                                               |  x   |   x   |   x   |\n| Series name, value/category refs, fill color, line stroke (width / dash)                                                     |  x   |   x   |   x   |\n| Series markers (symbol / size / fill / outline)                                                                              |  x   |   x   |   x   |\n| Data labels — chart-level + per-series (show\\*, position, separator, number format, leader lines, typography, fill / border) |  x   |   x   |   x   |\n| Data table (showHorzBorder / showVertBorder / showOutline / showKeys, typography, fill / border)                             |  x   |   x   |   x   |\n| Bar / column gap-width and overlap                                                                                           |  x   |   x   |   x   |\n| Pie / doughnut hole size, vary-colors, first-slice angle                                                                     |  x   |   x   |   x   |\n| Display blanks-as, plot-vis-only, show-data-labels-over-max, lang, date1904                                                  |  x   |   x   |   x   |\n| 3D walls / floor / view3D                                                                                                    |  x   |   x   |   x   |\n| Chart-space protection block                                                                                                 |  x   |   x   |   x   |\n| Anchor (twoCellAnchor / oneCellAnchor) + relative offsets                                                                    |  x   |   x   |   x   |\n\n#### Read side — `parseChart` / `getCharts`\n\n```ts\nimport { getCharts, openXlsx, parseChart } from \"hucre\"\n\nconst wb = await openXlsx(buf)\n\nfor (const { sheetName, chart } of getCharts(wb)) {\n  console.log(sheetName, chart.kinds, chart.title)\n  // e.g. \"Sales\" [\"bar\"] \"Quarterly Sales\"\n  console.log(chart.anchor) // { from: { row: 1, col: 3 }, to: { row: 16, col: 10 } }\n  console.log(chart.axes?.x?.title, chart.axes?.y?.scale)\n\n  for (const s of chart.series ?? []) {\n    console.log(s.kind, s.name, s.valuesRef, s.color, s.dataLabels)\n  }\n}\n\n// Standalone parser when you already have the chart XML.\nconst chart = parseChart(xml)\n```\n\n`Chart.kinds` lists every chart-type element under `\u003cc:plotArea\u003e` in\ndeclaration order, so combo charts surface as e.g. `[\"bar\", \"line\"]`.\n`Chart.series` mirrors the field shape that `ChartSeries` accepts on\nthe write side — a parsed series can be fed straight back into\n`SheetChart.series`. Bubble/scatter `\u003cc:numLit\u003e` series (literal\nembedded data, no formula) intentionally surface no\n`valuesRef` / `categoriesRef`. `Chart.anchor` mirrors\n`SheetChart.anchor`: `twoCellAnchor` charts surface both `from` and\n`to`, `oneCellAnchor` charts surface `from` only,\n`absoluteAnchor` charts (EMU-positioned, no cell anchor) report\n`anchor` as `undefined`.\n\n#### Write side — `writeXlsx` + `addChart`\n\n```ts\nimport { addChart, writeXlsx } from \"hucre\"\n\nconst dashboard = {\n  name: \"Dashboard\",\n  rows: [\n    [\"Quarter\", \"Revenue\", \"Forecast\"],\n    [\"Q1\", 12000, 11500],\n    [\"Q2\", 15500, 15000],\n    [\"Q3\", 14000, 14500],\n    [\"Q4\", 17800, 17200],\n  ],\n}\n\naddChart(dashboard, {\n  type: \"column\",\n  title: \"Quarterly Revenue\",\n  titleFontSize: 14,\n  titleBold: true,\n  titleColor: \"1F77B4\",\n  titleBorderColor: \"1F77B4\",\n  titleBorderWidth: 1.5,\n  titleBorderDash: \"dash\",\n  series: [\n    { name: \"Revenue\", values: \"B2:B5\", categories: \"A2:A5\", color: \"1F77B4\" },\n    { name: \"Forecast\", values: \"C2:C5\", categories: \"A2:A5\", color: \"FF7F0E\" },\n  ],\n  axes: {\n    x: { title: \"Quarter\" },\n    y: { title: \"Revenue (USD)\", numberFormat: { formatCode: \"$#,##0\" } },\n  },\n  legend: \"bottom\",\n  legendFillColor: \"F2F2F2\",\n  legendBorderDash: \"dot\",\n  plotAreaBorderColor: \"DDDDDD\",\n  plotAreaBorderWidth: 0.75,\n  dataLabels: { showValue: true, position: \"outEnd\", fontSize: 9 },\n  anchor: { from: { row: 6, col: 0 }, to: { row: 22, col: 7 } },\n})\n\nconst xlsx = await writeXlsx({ sheets: [dashboard] })\n```\n\nEvery knob the writer accepts lives on the `SheetChart` type — see\n[`SheetChart` in `src/_types.ts`](./src/_types.ts) for the full\nfield list with per-field semantics, OOXML mapping, and clamp /\ndefault behavior. The capability table above lists the families\ncovered.\n\n#### Clone — `cloneChart(source, options)`\n\n`cloneChart` takes a parsed `Chart` and a `CloneChartOptions` override\nbag and returns a `SheetChart` ready for `writeXlsx`. Every override\nfield uses the same grammar:\n\n- `undefined` (or omitted) — inherit the source value.\n- `null` — drop the source value (writer falls back to the OOXML\n  default for that slot).\n- a typed value — replace the source value (after running through the\n  same clamp / normalize as the writer).\n\nPer-series overrides are supplied as a positional `series` array;\neach entry merges with the source series at the matching index.\n\n```ts\nimport { cloneChart, openXlsx, parseChart, writeXlsx } from \"hucre\"\n\nconst wb = await openXlsx(templateBytes)\nconst sourceChart = wb.sheets[0].charts?.[0]\nif (!sourceChart) throw new Error(\"template missing chart\")\n\n// Re-bind the template chart to a new data range and recolour series.\nconst cloned = cloneChart(sourceChart, {\n  anchor: { from: { row: 0, col: 0 }, to: { row: 18, col: 8 } },\n  title: \"FY 2026 Revenue by Region\",\n  titleColor: \"1F77B4\",\n  legend: \"right\",\n  series: [\n    { values: \"B2:B13\", categories: \"A2:A13\", color: \"1F77B4\" },\n    { values: \"C2:C13\", categories: \"A2:A13\", color: null /* drop template tint */ },\n  ],\n})\n\nconst out = await writeXlsx({\n  sheets: [{ name: \"Sheet1\", rows: dashboardRows, charts: [cloned] }],\n})\n```\n\nA common pattern is \"template -\u003e override -\u003e write\" — keep one\nchart-of-each-flavour template and use `cloneChart` to spawn many\nvariants without re-encoding the whole `\u003cc:chartSpace\u003e` tree by\nhand. See [`CloneChartOptions` in\n`src/xlsx/chart-clone.ts`](./src/xlsx/chart-clone.ts) for the full\noverride surface (every knob on `SheetChart` has a matching\n`undefined | null | value` override field).\n\n#### Walking and adding charts\n\n```ts\nimport { addChart, getCharts, openXlsx, writeXlsx } from \"hucre\"\n\nconst wb = await openXlsx(templateBytes)\n\n// Read side — find every chart in a template workbook.\nfor (const { sheetName, chart } of getCharts(wb)) {\n  console.log(sheetName, chart.kinds, chart.title)\n}\n\n// Write side — declarative chart attachment.\nconst dashboard = { name: \"Dashboard\", rows: dashboardRows }\naddChart(dashboard, {\n  type: \"column\",\n  title: \"Q1 Revenue\",\n  series: [{ name: \"Revenue\", values: \"B2:B13\", categories: \"A2:A13\" }],\n  anchor: { from: { row: 14, col: 0 } },\n})\nawait writeXlsx({ sheets: [dashboard] })\n```\n\nCharts also survive the **roundtrip** (`openXlsx` → modify → `saveXlsx`),\nnot just the fresh `writeXlsx` path. A chart attached to a newly added\nsheet — or carried across workbooks by `copySheetToWorkbook` — is\nserialized into proper `xl/charts/chartN.xml` parts with their drawing\nrelationships on save:\n\n```ts\nimport { copySheetToWorkbook, getCharts, openXlsx, saveXlsx } from \"hucre\"\n\nconst template = await openXlsx(templateBytes)\nconst report = await openXlsx(reportBytes)\n\n// Copy a chart-bearing sheet from the template into the report workbook…\ncopySheetToWorkbook(template.sheets[0], report, \"Dashboard\")\n\n// …and saveXlsx re-emits the chart parts (not just the cell data).\nconst out = await saveXlsx(report)\nconsole.log(getCharts(await openXlsx(out)).length) // includes the copied chart\n```\n\nThe roundtrip path serializes model charts for sheets that don't already\nown a drawing (new or copied sheets); to add a chart to a sheet that\nalready carries one, compose it through the fresh `writeXlsx` path\ninstead.\n\n### Unified API\n\nAuto-detect format and work with simple helpers:\n\n```ts\nimport { read, write, readObjects, writeObjects } from \"hucre\"\n\n// Auto-detect XLSX vs ODS\nconst wb = await read(buffer)\n\n// Quick: file → array of objects\nconst products = await readObjects\u003c{ name: string; price: number }\u003e(buffer)\n\n// Quick: objects → XLSX\nconst xlsx = await writeObjects(products, { sheetName: \"Products\" })\n```\n\n### CLI\n\n```bash\nnpx hucre convert input.xlsx output.csv\nnpx hucre convert input.csv output.xlsx\nnpx hucre inspect file.xlsx\nnpx hucre inspect file.xlsx --sheet 0\nnpx hucre validate data.xlsx --schema schema.json\n```\n\n### Sheet Operations\n\nManipulate sheet data in memory:\n\n```ts\nimport { insertRows, deleteRows, cloneSheet, moveSheet } from \"hucre\"\n\ninsertRows(sheet, 5, 3) // Insert 3 rows at position 5\ndeleteRows(sheet, 0, 1) // Delete first row\nconst copy = cloneSheet(sheet, \"Copy\") // Deep clone\nmoveSheet(workbook, 0, 2) // Reorder sheets\n```\n\n### HTML \u0026 Markdown Export\n\n```ts\nimport { toHtml, toMarkdown } from \"hucre\"\n\nconst html = toHtml(workbook.sheets[0], {\n  headerRow: true,\n  styles: true,\n  classes: true,\n})\n\nconst md = toMarkdown(workbook.sheets[0])\n// | Name   | Price  | Stock |\n// |--------|-------:|------:|\n// | Widget |   9.99 |   142 |\n```\n\n### Number Format Renderer\n\n```ts\nimport { formatValue } from \"hucre\"\n\nformatValue(1234.5, \"#,##0.00\") // \"1,234.50\"\nformatValue(0.15, \"0%\") // \"15%\"\nformatValue(44197, \"yyyy-mm-dd\") // \"2021-01-01\"\nformatValue(1234, \"$#,##0\") // \"$1,234\"\nformatValue(0.333, \"# ?/?\") // \"1/3\"\n```\n\n### Cell Utilities\n\n```ts\nimport { parseCellRef, cellRef, colToLetter, rangeRef } from \"hucre\"\n\nparseCellRef(\"AA15\") // { row: 14, col: 26 }\ncellRef(14, 26) // \"AA15\"\ncolToLetter(26) // \"AA\"\nrangeRef(0, 0, 9, 3) // \"A1:D10\"\n```\n\n### Builder API\n\nFluent method-chaining interface:\n\n```ts\nimport { WorkbookBuilder } from \"hucre\"\n\nconst xlsx = await WorkbookBuilder.create()\n  .addSheet(\"Products\")\n  .columns([\n    { header: \"Name\", key: \"name\", autoWidth: true },\n    { header: \"Price\", key: \"price\", numFmt: \"$#,##0.00\" },\n  ])\n  .row([\"Widget\", 9.99])\n  .row([\"Gadget\", 24.5])\n  .freeze(1)\n  .done()\n  .build()\n```\n\n### Template Engine\n\nFill `{{placeholders}}` in existing XLSX templates:\n\n```ts\nimport { openXlsx, saveXlsx, fillTemplate } from \"hucre\"\n\nconst workbook = await openXlsx(templateBuffer)\nfillTemplate(workbook, {\n  company: \"Acme Inc\",\n  date: new Date(),\n  total: 12500,\n})\nconst output = await saveXlsx(workbook)\n```\n\n### Excel 2024 Checkboxes\n\nBoolean cells can be flagged as native Excel 2024 checkboxes via Microsoft's\nFeaturePropertyBag extension. The cell value drives the checked state; older\nExcel and LibreOffice fall back to the raw `TRUE`/`FALSE` display since the\non-disk value is just a normal boolean.\n\n```ts\nimport { writeXlsx, readXlsx } from \"hucre/xlsx\"\n\nconst buf = await writeXlsx({\n  sheets: [\n    {\n      name: \"Tasks\",\n      rows: [[\"Done?\"], [true], [false], [true]],\n      cells: new Map([\n        [\"1,0\", { value: true, type: \"boolean\", checkbox: true }],\n        [\"2,0\", { value: false, type: \"boolean\", checkbox: true }],\n        [\"3,0\", { value: true, type: \"boolean\", checkbox: true }],\n      ]),\n    },\n  ],\n})\n\nconst wb = await readXlsx(buf)\nwb.sheets[0].cells?.get(\"1,0\")?.checkbox // true\n```\n\nThis is the first JS/TS implementation of native checkboxes — only `XlsxWriter`\n(Python) and `rust_xlsxwriter` had it before.\n\n### Accessibility (WCAG 2.1 AA)\n\nGenerate screen-reader-friendly spreadsheets and audit them for common\nWCAG 2.1 AA issues. Alt text on images and text boxes round-trips\nthrough `xdr:cNvPr/@descr` and `@title` (the OOXML attributes Excel and\nassistive tech read), and per-sheet summaries can promote the first\nnon-empty value into `docProps/core.xml` so screen readers announce it\non file open.\n\n```ts\nimport { writeXlsx, a11y, readXlsx } from \"hucre\"\n\nconst xlsx = await writeXlsx({\n  sheets: [\n    {\n      name: \"Q1 Sales\",\n      rows: [\n        [\"Region\", \"Revenue\"],\n        [\"EU\", 12_400],\n      ],\n      a11y: { summary: \"Quarterly sales by region\", headerRow: 0 },\n      images: [\n        {\n          data: pngBytes,\n          type: \"png\",\n          anchor: { from: { row: 0, col: 3 } },\n          altText: \"Bar chart showing 47% YoY growth\",\n        },\n      ],\n    },\n  ],\n})\n\n// Audit a workbook for missing alt text, missing header rows,\n// merged headers, low contrast, and more.\nconst wb = await readXlsx(xlsx)\nfor (const issue of a11y.audit(wb)) {\n  console.log(issue.type, issue.code, issue.message, issue.location)\n}\n\n// Color contrast helpers (WCAG 2.1 sRGB)\na11y.contrastRatio(\"0969DA\", \"FFFFFF\") // ≈ 4.93 (passes AA)\na11y.relativeLuminance(\"808080\")\n```\n\nIssue codes: `no-doc-title`, `no-doc-description`, `empty-sheet`,\n`no-header-row`, `merged-header-row`, `missing-alt-text` (error for\nimages, warning for text boxes), `low-contrast`, `blank-row-in-data`.\nTune the contrast pass with\n`audit(wb, { skipContrast, minContrast, contrastSampleLimit })`.\n\n### Object Shorthand (XLSX / ODS)\n\nSkip the `wb.sheets[0].rows[0] as headers, slice(1) as data` boilerplate — return objects directly, mirror of `parseCsvObjects`:\n\n```ts\nimport { readXlsxObjects, writeXlsxObjects } from \"hucre/xlsx\"\nimport { readOdsObjects, writeOdsObjects } from \"hucre/ods\"\n\nconst { data, headers } = await readXlsxObjects(buffer, {\n  sheet: 0, // index or name (default: 0)\n  headerRow: 0, // 0-based (default: 0)\n  skipEmptyRows: true,\n  transformHeader: (h) =\u003e h.toLowerCase().replace(/ /g, \"_\"),\n  transformValue: (v, header) =\u003e (header === \"price\" ? Number(v) : v),\n})\n\n// Symmetric write — headers come from the first object's keys when omitted\nconst xlsx = await writeXlsxObjects(\n  [\n    { Name: \"Widget\", Price: 9.99 },\n    { Name: \"Gadget\", Price: 24.5 },\n  ],\n  { sheetName: \"Products\" },\n)\n```\n\n### JSON / NDJSON\n\n```ts\nimport {\n  parseJson,\n  parseNdjson,\n  writeJson,\n  writeNdjson,\n  workbookToJson,\n  NdjsonStreamWriter,\n  readNdjsonStream,\n} from \"hucre/json\"\n\n// Read — top-level array, { products: [...] } shape, or single object\nconst { data, headers } = parseJson(jsonString)\n\n// Pick rows from a deeper path\nparseJson(text, { rowsAt: \"data.rows\" })\n\n// Flatten nested objects with dot-path keys (default: true)\nparseJson('[{\"sku\":\"P1\",\"pricing\":{\"cost\":100}}]')\n// → data: [{ sku: \"P1\", \"pricing.cost\": 100 }]\n\n// NDJSON / JSON Lines — one object per line\nconst out = parseNdjson(ndjsonText, {\n  onError: (line, ln) =\u003e console.warn(`bad line ${ln}`), // skip + report\n})\n\n// Round-trip a workbook (single sheet → array, multi-sheet → { Sheet: [...] })\nimport { readXlsx } from \"hucre/xlsx\"\nconst wb = await readXlsx(buffer)\nconst json = workbookToJson(wb, { pretty: true })\n\n// Streaming write — works in Cloudflare Workers / Deno / Node 18+\nconst writer = new NdjsonStreamWriter()\nfor await (const row of source) writer.write(row)\nwriter.end()\nreturn new Response(writer.toStream(), {\n  headers: { \"content-type\": \"application/x-ndjson\" },\n})\n\n// Streaming read\nfor await (const row of readNdjsonStream(request.body!)) {\n  console.log(row)\n}\n```\n\n### XML\n\nRead and write tabular XML — product feeds (GS1 GDSN, Trendyol, marketplace exports), ERP dumps (SAP B1, Logo GO, Netsis), CRM catalogs. SAX-based: 50–200 MB feeds don't load into memory.\n\n```ts\nimport { readXml, writeXml } from \"hucre/xml\"\n\n// Auto-detects the most-frequently-repeating direct child of root as the row tag\nconst { data, headers, rowTag } = readXml(`\n  \u003cCatalog\u003e\n    \u003cProduct code=\"P1\"\u003e\n      \u003cName\u003eOak\u003c/Name\u003e\n      \u003cPricing currency=\"USD\"\u003e\n        \u003cCost\u003e100\u003c/Cost\u003e\n        \u003cRetail\u003e180\u003c/Retail\u003e\n      \u003c/Pricing\u003e\n    \u003c/Product\u003e\n    \u003cProduct code=\"P2\"\u003e\u003cName\u003ePine\u003c/Name\u003e\u003c/Product\u003e\n  \u003c/Catalog\u003e\n`)\n// rowTag: \"Product\"\n// data: [{ \"@code\": \"P1\", Name: \"Oak\", \"Pricing.@currency\": \"USD\",\n//         \"Pricing.Cost\": \"100\", \"Pricing.Retail\": \"180\" }, ...]\n\n// Override auto-detect with rowTag, strip namespace prefixes, control flatten\nreadXml(xml, { rowTag: \"ns:Product\", stripNamespaces: true, flatten: true })\n\n// Write — @-keyed fields become XML attributes, dot-paths reconstruct elements\nconst xml = writeXml(\n  [\n    { \"@code\": \"P1\", Name: \"Oak\", \"Pricing.Cost\": 100 },\n    { \"@code\": \"P2\", Name: \"Pine\", \"Pricing.Cost\": 90 },\n  ],\n  { rootTag: \"Catalog\", rowTag: \"Product\", pretty: true },\n)\n```\n\n### JSON Export (legacy)\n\n```ts\nimport { toJson } from \"hucre\"\n\ntoJson(sheet, { format: \"objects\" }) // [{Name:\"Widget\", Price:9.99}, ...]\ntoJson(sheet, { format: \"columns\" }) // {Name:[\"Widget\"], Price:[9.99]}\ntoJson(sheet, { format: \"arrays\" }) // {headers:[...], data:[[...]]}\n```\n\nFor new code prefer `writeJson` / `workbookToJson` from `hucre/json` — same result, consistent with `parseJson`/`parseNdjson`/`writeNdjson`.\n\n### CSV\n\n```ts\nimport { parseCsv, parseCsvObjects, writeCsv, detectDelimiter } from \"hucre/csv\"\n\n// Parse — auto-detects delimiter, handles RFC 4180 edge cases\nconst rows = parseCsv(csvString, { typeInference: true })\n\n// Parse with headers — returns typed objects\nconst { data, headers } = parseCsvObjects(csvString, { header: true })\n\n// Write\nconst csv = writeCsv(rows, { delimiter: \";\", bom: true })\n\n// Detect delimiter\ndetectDelimiter(csvString) // \",\" or \";\" or \"\\t\" or \"|\"\n```\n\n### Schema Validation\n\nValidate imported data with type coercion, pattern matching, and error collection:\n\n```ts\nimport { validateWithSchema } from \"hucre\"\nimport { parseCsv } from \"hucre/csv\"\n\nconst rows = parseCsv(csvString)\n\nconst result = validateWithSchema(\n  rows,\n  {\n    \"Product Name\": { type: \"string\", required: true },\n    Price: { type: \"number\", required: true, min: 0 },\n    SKU: { type: \"string\", pattern: /^[A-Z]{3}-\\d{4}$/ },\n    Stock: { type: \"integer\", min: 0, default: 0 },\n    Status: { type: \"string\", enum: [\"active\", \"inactive\", \"draft\"] },\n  },\n  { headerRow: 1 },\n)\n\nconsole.log(result.data) // Validated \u0026 coerced objects\nconsole.log(result.errors) // [{ row: 3, field: \"Price\", message: \"...\", value: \"abc\" }]\n```\n\nSchema field options:\n\n| Option        | Type                                                       | Description                             |\n| ------------- | ---------------------------------------------------------- | --------------------------------------- |\n| `type`        | `\"string\" \\| \"number\" \\| \"integer\" \\| \"boolean\" \\| \"date\"` | Target type (with coercion)             |\n| `required`    | `boolean`                                                  | Reject null/empty values                |\n| `pattern`     | `RegExp`                                                   | Regex validation (strings)              |\n| `min`         | `number`                                                   | Min value (numbers) or length (strings) |\n| `max`         | `number`                                                   | Max value (numbers) or length (strings) |\n| `enum`        | `unknown[]`                                                | Allowed values                          |\n| `default`     | `unknown`                                                  | Default for null/empty                  |\n| `validate`    | `(v) =\u003e boolean \\| string`                                 | Custom validator                        |\n| `transform`   | `(v) =\u003e unknown`                                           | Post-validation transform               |\n| `column`      | `string`                                                   | Column header name                      |\n| `columnIndex` | `number`                                                   | Column index (0-based)                  |\n\n### Date Utilities\n\nTimezone-safe Excel date serial number conversion:\n\n```ts\nimport { serialToDate, dateToSerial, isDateFormat, formatDate } from \"hucre\"\n\nserialToDate(44197) // 2021-01-01T00:00:00.000Z\ndateToSerial(new Date(\"2021-01-01\")) // 44197\nisDateFormat(\"yyyy-mm-dd\") // true\nisDateFormat(\"#,##0.00\") // false\nformatDate(new Date(), \"yyyy-mm-dd\") // \"2026-03-24\"\n```\n\nHandles the Lotus 1-2-3 bug (serial 60), 1900/1904 date systems, and time fractions correctly.\n\n## Platform Support\n\nhucre works everywhere — no Node.js APIs (`fs`, `crypto`, `Buffer`) in core.\n\n| Runtime               | Status       |\n| --------------------- | ------------ |\n| Node.js 18+           | Full support |\n| Deno                  | Full support |\n| Bun                   | Full support |\n| Modern browsers       | Full support |\n| Cloudflare Workers    | Full support |\n| Vercel Edge Functions | Full support |\n| Web Workers           | Full support |\n\n## Architecture\n\n```\nhucre (~37 KB gzipped)\n├── zip/            Zero-dep DEFLATE/inflate + ZIP read/write\n├── xml/            SAX parser + XML writer (CSP-compliant, no eval)\n├── xlsx/\n│   ├── reader      Shared strings, styles, worksheets, relationships\n│   ├── writer      Styles, shared strings, drawing, tables, comments\n│   ├── roundtrip   Open → modify → save with preservation\n│   ├── stream-*    Streaming reader (AsyncGenerator) + writer\n│   └── auto-width  Font-aware column width calculation\n├── ods/            OpenDocument Spreadsheet read/write\n├── csv/            RFC 4180 parser/writer + streaming\n├── export/         HTML, Markdown, JSON, TSV output + HTML import\n├── hucre           Unified read/write API, format auto-detect\n├── builder         Fluent WorkbookBuilder / SheetBuilder API\n├── template        {{placeholder}} template engine\n├── sheet-ops       Insert/delete/move/sort/find/replace, clone, copy\n├── cell-utils      parseCellRef, colToLetter, parseRange, isInRange\n├── image           imageFromBase64 utility\n├── worker          Web Worker serialization helpers\n├── _date           Timezone-safe serial ↔ Date, Lotus bug, 1900/1904\n├── _format         Number format renderer (locale-aware)\n├── _schema         Schema validation, type coercion, error collection\n└── cli             Convert, inspect, validate (citty + consola)\n```\n\nZero dependencies. Pure TypeScript. The ZIP engine uses `CompressionStream`/`DecompressionStream` Web APIs with a pure TS fallback.\n\n## API Reference\n\n### High-level\n\n| Function                       | Description                                       |\n| ------------------------------ | ------------------------------------------------- |\n| `read(input, options?)`        | Auto-detect format (XLSX/ODS), returns `Workbook` |\n| `write(options)`               | Write XLSX or ODS (via `format` option)           |\n| `readObjects(input, options?)` | File → array of objects (first row = headers)     |\n| `writeObjects(data, options?)` | Objects → XLSX/ODS                                |\n\n### XLSX\n\n| Function                           | Description                                                                 |\n| ---------------------------------- | --------------------------------------------------------------------------- |\n| `readXlsx(input, options?)`        | Parse XLSX from `Uint8Array \\| ArrayBuffer \\| ReadableStream\u003cUint8Array\u003e`   |\n| `writeXlsx(options)`               | Generate XLSX, returns `Uint8Array`                                         |\n| `readXlsxObjects(input, options?)` | Read sheet as `{ data, headers }` — mirror of CSV                           |\n| `writeXlsxObjects(data, options?)` | Write objects to XLSX (auto-derives headers from keys)                      |\n| `openXlsx(input, options?)`        | Open for round-trip (preserves unknown parts)                               |\n| `saveXlsx(workbook)`               | Save round-trip workbook back to XLSX                                       |\n| `streamXlsxRows(input, options?)`  | AsyncGenerator yielding rows one at a time                                  |\n| `XlsxStreamWriter`                 | Incremental row-by-row XLSX writing; auto-splits past `maxRowsPerSheet`     |\n| `XLSX_MAX_ROWS_PER_SHEET`          | Excel hard row limit (1,048,576) — exported constant                        |\n| `parseExternalLink(xml, relsXml?)` | Parse `xl/externalLinks/externalLinkN.xml` → `ExternalLink`                 |\n| `parseCellImages(xml)`             | Parse `xl/cellimages.xml` → `ParsedCellImageRef[]` (WPS DISPIMG)            |\n| `assembleCellImages(refs, media)`  | Combine parsed refs with resolved media bytes → `CellImage[]`               |\n| `parseSlicers(xml)`                | Parse `xl/slicers/slicerN.xml` → `Slicer[]`                                 |\n| `parseSlicerCache(xml)`            | Parse `xl/slicerCaches/slicerCacheN.xml` → `SlicerCache \\| undefined`       |\n| `parseTimelines(xml)`              | Parse `xl/timelines/timelineN.xml` → `Timeline[]`                           |\n| `parseTimelineCache(xml)`          | Parse `xl/timelineCaches/timelineCacheN.xml` → `TimelineCache \\| undefined` |\n| `parsePivotTable(xml)`             | Parse `xl/pivotTables/pivotTableN.xml` → `PivotTable \\| undefined`          |\n| `parsePivotCacheDefinition(xml)`   | Parse `xl/pivotCache/pivotCacheDefinitionN.xml` → `PivotCache \\| undefined` |\n| `attachPivotCacheFields(pt, c)`    | Overlay `PivotCache.fieldNames` onto a `PivotTable.fields[].name`           |\n| `parseChart(xml)`                  | Parse `xl/charts/chartN.xml` → `Chart \\| undefined`                         |\n| `cloneChart(source, options)`      | Convert a parsed `Chart` into a writer-ready `SheetChart`                   |\n| `chartKindToWriteKind(kind)`       | Map a read-side `ChartKind` onto its writable counterpart, if any           |\n| `getCharts(workbook)`              | Enumerate every chart anchored on the workbook with its sheet context       |\n| `addChart(sheet, chart)`           | Append a `SheetChart` to a `WriteSheet`, lazily creating the array          |\n\n### ODS\n\n| Function                          | Description                                                           |\n| --------------------------------- | --------------------------------------------------------------------- |\n| `readOds(input, options?)`        | Parse ODS (`Uint8Array \\| ArrayBuffer \\| ReadableStream\u003cUint8Array\u003e`) |\n| `writeOds(options)`               | Generate ODS                                                          |\n| `readOdsObjects(input, options?)` | Read sheet as `{ data, headers }`                                     |\n| `writeOdsObjects(data, options?)` | Write objects to ODS                                                  |\n| `streamOdsRows(input)`            | AsyncGenerator yielding ODS rows                                      |\n\n### CSV\n\n| Function                           | Description                                  |\n| ---------------------------------- | -------------------------------------------- |\n| `parseCsv(input, options?)`        | Parse CSV string → `CellValue[][]`           |\n| `parseCsvObjects(input, options?)` | Parse CSV with headers → `{ data, headers }` |\n| `writeCsv(rows, options?)`         | Write `CellValue[][]` → CSV string           |\n| `writeCsvObjects(data, options?)`  | Write objects → CSV string                   |\n| `detectDelimiter(input)`           | Auto-detect delimiter character              |\n| `streamCsvRows(input, options?)`   | Generator yielding CSV rows                  |\n| `CsvStreamWriter`                  | Class for incremental CSV writing            |\n| `writeTsv(rows, options?)`         | Write TSV (tab-separated)                    |\n| `fetchCsv(url, options?)`          | Fetch and parse CSV from URL                 |\n\n### JSON\n\n| Function                          | Description                                                    |\n| --------------------------------- | -------------------------------------------------------------- |\n| `parseJson(input, options?)`      | Parse JSON string/Uint8Array → `{ data, headers }`             |\n| `parseValue(value, options?)`     | Same on already-parsed JSON                                    |\n| `parseNdjson(input, options?)`    | Parse NDJSON / JSON Lines (`onError` skips invalid)            |\n| `writeJson(data, options?)`       | Serialize rows to a JSON string                                |\n| `writeNdjson(data, options?)`     | Serialize rows to NDJSON, one object per line                  |\n| `workbookToJson(wb, options?)`    | Convert a `Workbook` to JSON (single-sheet array or per-sheet) |\n| `readNdjsonStream(stream, opts?)` | Async generator over a `ReadableStream\u003cUint8Array\u003e`            |\n| `NdjsonStreamWriter`              | Incremental writer with `toStream(): ReadableStream`           |\n\n### XML\n\n| Function                   | Description                                              |\n| -------------------------- | -------------------------------------------------------- |\n| `readXml(input, options?)` | SAX-based XML reader, auto-detects repeating row element |\n| `writeXml(data, options?)` | Serialize rows to XML; `@`-keys → attributes             |\n\n### Sheet Operations\n\n| Function                                | Description                     |\n| --------------------------------------- | ------------------------------- |\n| `insertRows(sheet, index, count)`       | Insert rows, shift down         |\n| `deleteRows(sheet, index, count)`       | Delete rows, shift up           |\n| `insertColumns(sheet, index, count)`    | Insert columns, shift right     |\n| `deleteColumns(sheet, index, count)`    | Delete columns, shift left      |\n| `moveRows(sheet, from, count, to)`      | Move rows                       |\n| `cloneSheet(sheet, name)`               | Deep clone a sheet              |\n| `copySheetToWorkbook(sheet, wb, name?)` | Copy sheet between workbooks    |\n| `copyRange(sheet, source, target)`      | Copy cell range within sheet    |\n| `moveSheet(wb, from, to)`               | Reorder sheets                  |\n| `removeSheet(wb, index)`                | Remove a sheet                  |\n| `sortRows(sheet, col, order?)`          | Sort rows by column             |\n| `findCells(sheet, predicate)`           | Find cells by value or function |\n| `replaceCells(sheet, find, replace)`    | Find and replace values         |\n\n### Export\n\n| Function                      | Description                                      |\n| ----------------------------- | ------------------------------------------------ |\n| `toHtml(sheet, options?)`     | HTML `\u003ctable\u003e` with styles, a11y, dark/light CSS |\n| `toMarkdown(sheet, options?)` | Markdown table with auto-alignment               |\n| `toJson(sheet, options?)`     | JSON (objects, arrays, or columns format)        |\n| `fromHtml(html, options?)`    | Parse HTML table string → Sheet                  |\n| `writeTsv(rows, options?)`    | Write TSV (tab-separated)                        |\n\n### Builder\n\n| Function                       | Description                             |\n| ------------------------------ | --------------------------------------- |\n| `WorkbookBuilder.create()`     | Fluent API for building workbooks       |\n| `fillTemplate(workbook, data)` | Replace `{{placeholders}}` in templates |\n\n### Formatting \u0026 Utilities\n\n| Function                                     | Description                              |\n| -------------------------------------------- | ---------------------------------------- |\n| `formatValue(value, numFmt, options?)`       | Apply Excel number format (locale-aware) |\n| `validateWithSchema(rows, schema, options?)` | Validate \u0026 coerce data with schema       |\n| `serialToDate(serial, is1904?)`              | Excel serial → Date (UTC)                |\n| `dateToSerial(date, is1904?)`                | Date → Excel serial                      |\n| `isDateFormat(numFmt)`                       | Check if format string is date           |\n| `formatDate(date, format)`                   | Format Date with Excel format string     |\n| `parseCellRef(ref)`                          | \"AA15\" → `{ row: 14, col: 26 }`          |\n| `cellRef(row, col)`                          | `(14, 26)` → \"AA15\"                      |\n| `colToLetter(col)`                           | `26` → \"AA\"                              |\n| `rangeRef(r1, c1, r2, c2)`                   | `(0,0,9,3)` → \"A1:D10\"                   |\n\n### Accessibility (a11y)\n\n| Function                      | Description                                                |\n| ----------------------------- | ---------------------------------------------------------- |\n| `a11y.audit(wb, options?)`    | WCAG 2.1 AA audit; returns `A11yIssue[]`                   |\n| `a11y.contrastRatio(fg, bg)`  | sRGB contrast ratio (1–21) for two hex colors              |\n| `a11y.relativeLuminance(hex)` | WCAG relative luminance (0–1) for a hex color              |\n| `a11y.applyA11ySummary(wb)`   | Promote first sheet `a11y.summary` to workbook description |\n\n### Web Worker Helpers\n\n| Function                    | Description                                                          |\n| --------------------------- | -------------------------------------------------------------------- |\n| `serializeWorkbook(wb)`     | Convert Workbook for `postMessage` (Maps → objects, Dates → strings) |\n| `deserializeWorkbook(data)` | Restore Workbook from serialized form                                |\n| `WORKER_SAFE_FUNCTIONS`     | List of all hucre functions safe for Web Workers (all of them)       |\n\n## Development\n\n```sh\npnpm install\npnpm dev          # vitest watch\npnpm test         # lint + typecheck + test\npnpm build        # obuild (minified, tree-shaken)\npnpm lint:fix     # oxlint + oxfmt\npnpm typecheck    # tsgo\n```\n\n## Contributing\n\nContributions are welcome! Please [open an issue](https://github.com/productdevbook/hucre/issues) or submit a PR.\n\n127 of 135 tracked features are implemented. See the [issue tracker](https://github.com/productdevbook/hucre/issues) for the roadmap.\n\n### Roadmap\n\n**Upcoming Engine Features:**\n\n- Chart creation (bar, line, pie, scatter, area + subtypes) — synthesize from a fresh write (read + roundtrip already supported)\n- XLS BIFF8 read (legacy Excel 97-2003)\n- XLSB binary format read\n- Formula evaluation engine\n- File encryption/decryption (AES-256, MS-OFFCRYPTO)\n- Threaded comments (Excel 365+) — synthesize from a fresh write (read + roundtrip already supported)\n- Checkboxes (Excel 2024+)\n- VBA/macro injection\n- Slicers \u0026 timeline filters — synthesize from a fresh write (read + roundtrip already supported)\n- WPS DISPIMG cell-embedded images — synthesize from a fresh write (read + roundtrip already supported)\n- R1C1 notation support\n- Accessibility helpers (WCAG 2.1 AA)\n\n## Alternatives\n\nLooking for a different approach? These libraries may fit your use case:\n\n- **[SheetJS (xlsx)](https://github.com/SheetJS/sheetjs)** — The most popular spreadsheet library. Feature-rich but large bundle (~300 KB), removed from npm (CDN-only), styling requires Pro license.\n- **[ExcelJS](https://github.com/exceljs/exceljs)** — Read/write/stream XLSX with styling. Mature but has 12 dependencies (some with CVEs), CJS-only, no ESM.\n- **[xlsx-js-style](https://github.com/gitbrent/xlsx-js-style)** — SheetJS fork that adds cell styling. Same bundle size and limitations as SheetJS.\n- **[xlsmith](https://github.com/ChronicStone/xlsmith)** — Schema-driven Excel report builder with typed column definitions, formula helpers, conditional styles, and summary rows. Great for structured report generation.\n- **[xlsx-populate](https://github.com/dtjohnson/xlsx-populate)** — Template-based XLSX manipulation. Good for filling existing templates, limited write-from-scratch support.\n- **[better-xlsx](https://github.com/nichenqin/better-xlsx)** — Lightweight XLSX writer with styling. Write-only, no read support.\n\n## License\n\n[MIT](./LICENSE) — Made by [productdevbook](https://github.com/productdevbook)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fproductdevbook%2Fhucre","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fproductdevbook%2Fhucre","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fproductdevbook%2Fhucre/lists"}