[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-74983":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":23,"hasPages":23,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":41,"readmeContent":42,"aiSummary":43,"trendingCount":16,"starSnapshotCount":16,"syncStatus":44,"lastSyncTime":45,"discoverSource":46},74983,"hucre","productdevbook\u002Fhucre","productdevbook","Zero-dependency spreadsheet engine. Read & write XLSX, CSV, ODS. Pure TypeScript, works everywhere.","https:\u002F\u002Fhucre.productdevbook.com",null,"TypeScript",1397,33,1340,8,0,4,9,56,12,70.69,"MIT License",false,"main",[26,27,28,29,30,31,32,33,34,35,36,37,38,39,40],"csv","csv-parser","esm","excel","ods","ods-parser","parser","spreadsheet","streaming","tree-shakeable","typescript","xlsx","xlsx-parser","xlsx-writer","zero-dependency","2026-06-12 04:01:16","\u003Cp align=\"center\">\n  \u003Cbr>\n  \u003Cimg src=\".github\u002Fassets\u002Fcover.svg\" alt=\"hucre — Zero-dependency spreadsheet engine\" width=\"100%\">\n  \u003Cbr>\u003Cbr>\n  \u003Cb style=\"font-size: 2em;\">hucre\u003C\u002Fb>\n  \u003Cbr>\u003Cbr>\n  Zero-dependency spreadsheet engine.\n  \u003Cbr>\n  Read & write XLSX, CSV, ODS, JSON, NDJSON, XML. Schema validation, streaming, round-trip preservation. Pure TypeScript, works everywhere.\n  \u003Cbr>\u003Cbr>\n  \u003Ca href=\"https:\u002F\u002Fnpmjs.com\u002Fpackage\u002Fhucre\">\u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002Fhucre?style=flat&colorA=18181B&colorB=34d399\" alt=\"npm version\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fnpmjs.com\u002Fpackage\u002Fhucre\">\u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fdm\u002Fhucre?style=flat&colorA=18181B&colorB=34d399\" alt=\"npm downloads\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fbundlephobia.com\u002Fresult?p=hucre\">\u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbundlephobia\u002Fminzip\u002Fhucre?style=flat&colorA=18181B&colorB=34d399\" alt=\"bundle size\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fproductdevbook\u002Fhucre\u002Fblob\u002Fmain\u002FLICENSE\">\u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Flicense\u002Fproductdevbook\u002Fhucre?style=flat&colorA=18181B&colorB=34d399\" alt=\"license\">\u003C\u002Fa>\n\u003C\u002Fp>\n\n## Quick Start\n\n```sh\nnpm install hucre\n```\n\n```ts\nimport { readXlsx, writeXlsx } from \"hucre\"\n\n\u002F\u002F Read an XLSX file\nconst workbook = await readXlsx(buffer)\nconsole.log(workbook.sheets[0].rows)\n\n\u002F\u002F 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\u002Fxlsx\" \u002F\u002F XLSX only\nimport { parseCsv, writeCsv } from \"hucre\u002Fcsv\" \u002F\u002F CSV only (~2 KB gzipped)\nimport { readOds, writeOds } from \"hucre\u002Fods\" \u002F\u002F ODS only\nimport { parseJson, writeNdjson } from \"hucre\u002Fjson\" \u002F\u002F JSON \u002F NDJSON\nimport { readXml, writeXml } from \"hucre\u002Fxml\" \u002F\u002F Tabular XML\n```\n\n## Why hucre?\n\n### vs JavaScript \u002F 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\u002FODS\u002FCSV | XLSX only     | XLSX only       | XLSX only       | XLS\u002FXLSX          |\n| **Zero dependencies** | Yes          | lxml optional | No              | Yes             | No                |\n\n## Features\n\n### Reading\n\n```ts\nimport { readXlsx } from \"hucre\u002Fxlsx\"\n\nconst wb = await readXlsx(uint8Array, {\n  sheets: [0, \"Products\"], \u002F\u002F Filter sheets by index or name\n  readStyles: true, \u002F\u002F Parse cell styles\n  dateSystem: \"auto\", \u002F\u002F Auto-detect 1900\u002F1904\n})\n\nfor (const sheet of wb.sheets) {\n  console.log(sheet.name) \u002F\u002F \"Products\"\n  console.log(sheet.rows) \u002F\u002F CellValue[][]\n  console.log(sheet.merges) \u002F\u002F 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\u002FO cost of the full read:\n\n```ts\nconst wb = await readXlsx(buf, {\n  sheets: (info) => !info.hidden && !info.veryHidden,\n})\n\u002F\u002F 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\u002Fxlsx\"\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\u002Fsplit panes, auto-filter with criteria, data validation, hyperlinks, images (PNG\u002FJPEG\u002FGIF\u002FSVG\u002FWebP), comments, tables, conditional formatting (cellIs\u002FcolorScale\u002FdataBar\u002FiconSet), named ranges, print settings, page breaks, sheet protection, workbook protection, rich text, shared\u002Farray\u002Fdynamic formulas, sparklines, textboxes, background images, number formats, hidden sheets, Excel 2024 native checkboxes, HTML\u002FMarkdown\u002FJSON\u002FTSV 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\u002Fmax 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:\u002F\u002Fgoogle.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\n### Streaming\n\nProcess large files row-by-row without loading everything into memory:\n\n```ts\nimport { streamXlsxRows, XlsxStreamWriter } from \"hucre\u002Fxlsx\"\n\n\u002F\u002F 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\u002F\u002F Cap the number of rows yielded (preview \u002F sampling). The underlying\n\u002F\u002F ZIP\u002FSAX stream is cancelled once the cap is reached, so very large\n\u002F\u002F sheets stay cheap.\nfor await (const row of streamXlsxRows(buffer, { maxRows: 100 })) {\n  console.log(row.index, row.values)\n}\n\n\u002F\u002F Filter to an A1 range. Rows outside the row span are skipped; cells\n\u002F\u002F outside the column span are masked to `null` (column indexes stay\n\u002F\u002F stable). Parsing stops once a row past the end-row is observed.\nfor await (const row of streamXlsxRows(buffer, { range: \"B2:D1000\" })) {\n  \u002F\u002F row.values[0] === null (column A is outside)\n  \u002F\u002F row.values[1..3] carry B\u002FC\u002FD\n}\n\n\u002F\u002F 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\u002Fxlsx\"\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, \u002F\u002F optional override; default = 1_048_576\n  repeatHeaders: true, \u002F\u002F default\n})\n\nfor (let i = 0; i \u003C 3_000_000; i++) writer.addRow([i + 1, Math.random()])\n\u002F\u002F → BigData, BigData_2, BigData_3\nconst buf = await writer.finish()\n```\n\n### ODS (OpenDocument)\n\n```ts\nimport { readOds, writeOds } from \"hucre\u002Fods\"\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\u002Fxlsx\"\n\nconst workbook = await openXlsx(buffer)\nworkbook.sheets[0].rows[0][0] = \"Updated!\"\nconst output = await saveXlsx(workbook) \u002F\u002F 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>` block and the matching relationship\ndisappear from `xl\u002Fworkbook.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      \u002F\u002F cell.type ∈ \"n\" | \"s\" | \"b\" | \"e\" | \"str\"\n      console.log(cell.ref, cell.type, cell.value)\n    }\n  }\n}\n\n\u002F\u002F 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\u002Fcellimages.xml` registry referenced from\n`=_xlfn.DISPIMG(\"\u003Cid>\", 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\u002F\u002F 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 & Timeline Filters\n\nSlicers (Excel 2010+) and timeline slicers (Excel 2013+) are read into\ntyped `workbook.slicerCaches` \u002F `workbook.timelineCaches` plus per-sheet\n`sheet.slicers` \u002F `sheet.timelines` arrays. On `saveXlsx` the slicer \u002F\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 \u002F timelines on next open.\n\n```ts\nimport { readXlsx } from \"hucre\"\n\nconst wb = await readXlsx(buf)\n\n\u002F\u002F Workbook-level cache definitions.\nconsole.log(wb.slicerCaches) \u002F\u002F SlicerCache[] (pivot-table or table source)\nconsole.log(wb.timelineCaches) \u002F\u002F TimelineCache[]\n\n\u002F\u002F Per-sheet slicer \u002F 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\u002F\u002F Standalone parsers when you already have the XML strings.\nimport { parseSlicers, parseSlicerCache, parseTimelines, parseTimelineCache } from \"hucre\"\n```\n\nThe worksheet body's `\u003Cx14:slicerList>` \u002F `\u003Cx15:timelines>` 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\u002FpivotTables\u002FpivotTableN.xml`) and their workbook-level\ncache definitions (`xl\u002FpivotCache\u002FpivotCacheDefinitionN.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>` 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\u002F\u002F Workbook-level cache definitions.\nfor (const cache of wb.pivotCaches ?? []) {\n  console.log(cache.cacheId, cache.sourceSheet, cache.sourceRef, cache.fieldNames)\n}\n\n\u002F\u002F 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\u002F\u002F 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\u002Fcharts\u002FchartN.xml` plus the optional `styleN.xml` \u002F\n`colorsN.xml` companions) round-trip through three layers:\n\n- `parseChart(xml)` \u002F `readXlsx().sheets[].charts[]` — surfaces a\n  read-side `Chart` record (kinds, title, series, axes, legend, data\n  labels, fills \u002F 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` \u002F `ChartDataLabelsInfo` \u002F `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` \u002F `null` \u002F value override grammar.\n\n| Family                                                                                                                       | Read | Write | Clone |\n| ---------------------------------------------------------------------------------------------------------------------------- | :--: | :---: | :---: |\n| Chart kinds (bar \u002F column \u002F line \u002F pie \u002F doughnut \u002F area \u002F scatter \u002F bubble \u002F combo)                                         |  x   |   x   |   x   |\n| Title text + visibility (`title`, `showTitle`, `autoTitleDeleted`)                                                           |  x   |   x   |   x   |\n| Title typography (size \u002F bold \u002F italic \u002F strike \u002F underline \u002F color \u002F family)                                                |  x   |   x   |   x   |\n| Title rotation, manual layout                                                                                                |  x   |   x   |   x   |\n| Title fill \u002F border color \u002F border width \u002F border dash                                                                       |  x   |   x   |   x   |\n| Legend position, overlay, font knobs, manual layout                                                                          |  x   |   x   |   x   |\n| Legend fill \u002F border color \u002F border width \u002F border dash, per-entry hide                                                      |  x   |   x   |   x   |\n| Plot-area manual layout, fill \u002F border color \u002F border width \u002F border dash                                                    |  x   |   x   |   x   |\n| Chart-space fill \u002F border color \u002F border width \u002F border dash, rounded corners, style preset                                  |  x   |   x   |   x   |\n| Axis title text + typography (per-axis), rotation, manual layout, fill \u002F border knobs                                        |  x   |   x   |   x   |\n| Axis label rotation, font size, bold \u002F italic \u002F underline \u002F strike, color, font family                                       |  x   |   x   |   x   |\n| Axis scale (min \u002F max \u002F logBase \u002F majorUnit \u002F minorUnit), reverse, hidden, crosses, dispUnits                                |  x   |   x   |   x   |\n| Axis number format, tick marks, tick-label position \u002F skip, label offset \u002F alignment                                         |  x   |   x   |   x   |\n| Axis gridlines (major \u002F minor)                                                                                               |  x   |   x   |   x   |\n| Series name, value\u002Fcategory refs, fill color, line stroke (width \u002F dash)                                                     |  x   |   x   |   x   |\n| Series markers (symbol \u002F size \u002F fill \u002F outline)                                                                              |  x   |   x   |   x   |\n| Data labels — chart-level + per-series (show\\*, position, separator, number format, leader lines, typography, fill \u002F border) |  x   |   x   |   x   |\n| Data table (showHorzBorder \u002F showVertBorder \u002F showOutline \u002F showKeys, typography, fill \u002F border)                             |  x   |   x   |   x   |\n| Bar \u002F column gap-width and overlap                                                                                           |  x   |   x   |   x   |\n| Pie \u002F 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 \u002F floor \u002F view3D                                                                                                    |  x   |   x   |   x   |\n| Chart-space protection block                                                                                                 |  x   |   x   |   x   |\n| Anchor (twoCellAnchor \u002F oneCellAnchor) + relative offsets                                                                    |  x   |   x   |   x   |\n\n#### Read side — `parseChart` \u002F `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  \u002F\u002F e.g. \"Sales\" [\"bar\"] \"Quarterly Sales\"\n  console.log(chart.anchor) \u002F\u002F { 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\u002F\u002F 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>` 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\u002Fscatter `\u003Cc:numLit>` series (literal\nembedded data, no formula) intentionally surface no\n`valuesRef` \u002F `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\u002F_types.ts`](.\u002Fsrc\u002F_types.ts) for the full\nfield list with per-field semantics, OOXML mapping, and clamp \u002F\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 \u002F 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\u002F\u002F 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 \u002F* drop template tint *\u002F },\n  ],\n})\n\nconst out = await writeXlsx({\n  sheets: [{ name: \"Sheet1\", rows: dashboardRows, charts: [cloned] }],\n})\n```\n\nA common pattern is \"template -> override -> write\" — keep one\nchart-of-each-flavour template and use `cloneChart` to spawn many\nvariants without re-encoding the whole `\u003Cc:chartSpace>` tree by\nhand. See [`CloneChartOptions` in\n`src\u002Fxlsx\u002Fchart-clone.ts`](.\u002Fsrc\u002Fxlsx\u002Fchart-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\u002F\u002F 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\u002F\u002F 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\n### Unified API\n\nAuto-detect format and work with simple helpers:\n\n```ts\nimport { read, write, readObjects, writeObjects } from \"hucre\"\n\n\u002F\u002F Auto-detect XLSX vs ODS\nconst wb = await read(buffer)\n\n\u002F\u002F Quick: file → array of objects\nconst products = await readObjects\u003C{ name: string; price: number }>(buffer)\n\n\u002F\u002F 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) \u002F\u002F Insert 3 rows at position 5\ndeleteRows(sheet, 0, 1) \u002F\u002F Delete first row\nconst copy = cloneSheet(sheet, \"Copy\") \u002F\u002F Deep clone\nmoveSheet(workbook, 0, 2) \u002F\u002F Reorder sheets\n```\n\n### HTML & 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\u002F\u002F | Name   | Price  | Stock |\n\u002F\u002F |--------|-------:|------:|\n\u002F\u002F | Widget |   9.99 |   142 |\n```\n\n### Number Format Renderer\n\n```ts\nimport { formatValue } from \"hucre\"\n\nformatValue(1234.5, \"#,##0.00\") \u002F\u002F \"1,234.50\"\nformatValue(0.15, \"0%\") \u002F\u002F \"15%\"\nformatValue(44197, \"yyyy-mm-dd\") \u002F\u002F \"2021-01-01\"\nformatValue(1234, \"$#,##0\") \u002F\u002F \"$1,234\"\nformatValue(0.333, \"# ?\u002F?\") \u002F\u002F \"1\u002F3\"\n```\n\n### Cell Utilities\n\n```ts\nimport { parseCellRef, cellRef, colToLetter, rangeRef } from \"hucre\"\n\nparseCellRef(\"AA15\") \u002F\u002F { row: 14, col: 26 }\ncellRef(14, 26) \u002F\u002F \"AA15\"\ncolToLetter(26) \u002F\u002F \"AA\"\nrangeRef(0, 0, 9, 3) \u002F\u002F \"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`\u002F`FALSE` display since the\non-disk value is just a normal boolean.\n\n```ts\nimport { writeXlsx, readXlsx } from \"hucre\u002Fxlsx\"\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 \u002F\u002F true\n```\n\nThis is the first JS\u002FTS 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\u002F@descr` and `@title` (the OOXML attributes Excel and\nassistive tech read), and per-sheet summaries can promote the first\nnon-empty value into `docProps\u002Fcore.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\u002F\u002F Audit a workbook for missing alt text, missing header rows,\n\u002F\u002F 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\u002F\u002F Color contrast helpers (WCAG 2.1 sRGB)\na11y.contrastRatio(\"0969DA\", \"FFFFFF\") \u002F\u002F ≈ 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 \u002F 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\u002Fxlsx\"\nimport { readOdsObjects, writeOdsObjects } from \"hucre\u002Fods\"\n\nconst { data, headers } = await readXlsxObjects(buffer, {\n  sheet: 0, \u002F\u002F index or name (default: 0)\n  headerRow: 0, \u002F\u002F 0-based (default: 0)\n  skipEmptyRows: true,\n  transformHeader: (h) => h.toLowerCase().replace(\u002F \u002Fg, \"_\"),\n  transformValue: (v, header) => (header === \"price\" ? Number(v) : v),\n})\n\n\u002F\u002F 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 \u002F NDJSON\n\n```ts\nimport {\n  parseJson,\n  parseNdjson,\n  writeJson,\n  writeNdjson,\n  workbookToJson,\n  NdjsonStreamWriter,\n  readNdjsonStream,\n} from \"hucre\u002Fjson\"\n\n\u002F\u002F Read — top-level array, { products: [...] } shape, or single object\nconst { data, headers } = parseJson(jsonString)\n\n\u002F\u002F Pick rows from a deeper path\nparseJson(text, { rowsAt: \"data.rows\" })\n\n\u002F\u002F Flatten nested objects with dot-path keys (default: true)\nparseJson('[{\"sku\":\"P1\",\"pricing\":{\"cost\":100}}]')\n\u002F\u002F → data: [{ sku: \"P1\", \"pricing.cost\": 100 }]\n\n\u002F\u002F NDJSON \u002F JSON Lines — one object per line\nconst out = parseNdjson(ndjsonText, {\n  onError: (line, ln) => console.warn(`bad line ${ln}`), \u002F\u002F skip + report\n})\n\n\u002F\u002F Round-trip a workbook (single sheet → array, multi-sheet → { Sheet: [...] })\nimport { readXlsx } from \"hucre\u002Fxlsx\"\nconst wb = await readXlsx(buffer)\nconst json = workbookToJson(wb, { pretty: true })\n\n\u002F\u002F Streaming write — works in Cloudflare Workers \u002F Deno \u002F 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\u002Fx-ndjson\" },\n})\n\n\u002F\u002F 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\u002Fxml\"\n\n\u002F\u002F Auto-detects the most-frequently-repeating direct child of root as the row tag\nconst { data, headers, rowTag } = readXml(`\n  \u003CCatalog>\n    \u003CProduct code=\"P1\">\n      \u003CName>Oak\u003C\u002FName>\n      \u003CPricing currency=\"USD\">\n        \u003CCost>100\u003C\u002FCost>\n        \u003CRetail>180\u003C\u002FRetail>\n      \u003C\u002FPricing>\n    \u003C\u002FProduct>\n    \u003CProduct code=\"P2\">\u003CName>Pine\u003C\u002FName>\u003C\u002FProduct>\n  \u003C\u002FCatalog>\n`)\n\u002F\u002F rowTag: \"Product\"\n\u002F\u002F data: [{ \"@code\": \"P1\", Name: \"Oak\", \"Pricing.@currency\": \"USD\",\n\u002F\u002F         \"Pricing.Cost\": \"100\", \"Pricing.Retail\": \"180\" }, ...]\n\n\u002F\u002F Override auto-detect with rowTag, strip namespace prefixes, control flatten\nreadXml(xml, { rowTag: \"ns:Product\", stripNamespaces: true, flatten: true })\n\n\u002F\u002F 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\" }) \u002F\u002F [{Name:\"Widget\", Price:9.99}, ...]\ntoJson(sheet, { format: \"columns\" }) \u002F\u002F {Name:[\"Widget\"], Price:[9.99]}\ntoJson(sheet, { format: \"arrays\" }) \u002F\u002F {headers:[...], data:[[...]]}\n```\n\nFor new code prefer `writeJson` \u002F `workbookToJson` from `hucre\u002Fjson` — same result, consistent with `parseJson`\u002F`parseNdjson`\u002F`writeNdjson`.\n\n### CSV\n\n```ts\nimport { parseCsv, parseCsvObjects, writeCsv, detectDelimiter } from \"hucre\u002Fcsv\"\n\n\u002F\u002F Parse — auto-detects delimiter, handles RFC 4180 edge cases\nconst rows = parseCsv(csvString, { typeInference: true })\n\n\u002F\u002F Parse with headers — returns typed objects\nconst { data, headers } = parseCsvObjects(csvString, { header: true })\n\n\u002F\u002F Write\nconst csv = writeCsv(rows, { delimiter: \";\", bom: true })\n\n\u002F\u002F Detect delimiter\ndetectDelimiter(csvString) \u002F\u002F \",\" 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\u002Fcsv\"\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: \u002F^[A-Z]{3}-\\d{4}$\u002F },\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) \u002F\u002F Validated & coerced objects\nconsole.log(result.errors) \u002F\u002F [{ 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\u002Fempty 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\u002Fempty                  |\n| `validate`    | `(v) => boolean \\| string`                                 | Custom validator                        |\n| `transform`   | `(v) => 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) \u002F\u002F 2021-01-01T00:00:00.000Z\ndateToSerial(new Date(\"2021-01-01\")) \u002F\u002F 44197\nisDateFormat(\"yyyy-mm-dd\") \u002F\u002F true\nisDateFormat(\"#,##0.00\") \u002F\u002F false\nformatDate(new Date(), \"yyyy-mm-dd\") \u002F\u002F \"2026-03-24\"\n```\n\nHandles the Lotus 1-2-3 bug (serial 60), 1900\u002F1904 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\u002F            Zero-dep DEFLATE\u002Finflate + ZIP read\u002Fwrite\n├── xml\u002F            SAX parser + XML writer (CSP-compliant, no eval)\n├── xlsx\u002F\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\u002F            OpenDocument Spreadsheet read\u002Fwrite\n├── csv\u002F            RFC 4180 parser\u002Fwriter + streaming\n├── export\u002F         HTML, Markdown, JSON, TSV output + HTML import\n├── hucre           Unified read\u002Fwrite API, format auto-detect\n├── builder         Fluent WorkbookBuilder \u002F SheetBuilder API\n├── template        {{placeholder}} template engine\n├── sheet-ops       Insert\u002Fdelete\u002Fmove\u002Fsort\u002Ffind\u002Freplace, 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\u002F1904\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`\u002F`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\u002FODS), 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\u002FODS                                |\n\n### XLSX\n\n| Function                           | Description                                                                 |\n| ---------------------------------- | --------------------------------------------------------------------------- |\n| `readXlsx(input, options?)`        | Parse XLSX from `Uint8Array \\| ArrayBuffer \\| ReadableStream\u003CUint8Array>`   |\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\u002FexternalLinks\u002FexternalLinkN.xml` → `ExternalLink`                 |\n| `parseCellImages(xml)`             | Parse `xl\u002Fcellimages.xml` → `ParsedCellImageRef[]` (WPS DISPIMG)            |\n| `assembleCellImages(refs, media)`  | Combine parsed refs with resolved media bytes → `CellImage[]`               |\n| `parseSlicers(xml)`                | Parse `xl\u002Fslicers\u002FslicerN.xml` → `Slicer[]`                                 |\n| `parseSlicerCache(xml)`            | Parse `xl\u002FslicerCaches\u002FslicerCacheN.xml` → `SlicerCache \\| undefined`       |\n| `parseTimelines(xml)`              | Parse `xl\u002Ftimelines\u002FtimelineN.xml` → `Timeline[]`                           |\n| `parseTimelineCache(xml)`          | Parse `xl\u002FtimelineCaches\u002FtimelineCacheN.xml` → `TimelineCache \\| undefined` |\n| `parsePivotTable(xml)`             | Parse `xl\u002FpivotTables\u002FpivotTableN.xml` → `PivotTable \\| undefined`          |\n| `parsePivotCacheDefinition(xml)`   | Parse `xl\u002FpivotCache\u002FpivotCacheDefinitionN.xml` → `PivotCache \\| undefined` |\n| `attachPivotCacheFields(pt, c)`    | Overlay `PivotCache.fieldNames` onto a `PivotTable.fields[].name`           |\n| `parseChart(xml)`                  | Parse `xl\u002Fcharts\u002FchartN.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>`) |\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\u002FUint8Array → `{ data, headers }`             |\n| `parseValue(value, options?)`     | Same on already-parsed JSON                                    |\n| `parseNdjson(input, options?)`    | Parse NDJSON \u002F 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>`            |\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>` with styles, a11y, dark\u002Flight 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 & Utilities\n\n| Function                                     | Description                              |\n| -------------------------------------------- | ---------------------------------------- |\n| `formatValue(value, numFmt, options?)`       | Apply Excel number format (locale-aware) |\n| `validateWithSchema(rows, schema, options?)` | Validate & 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:\u002F\u002Fgithub.com\u002Fproductdevbook\u002Fhucre\u002Fissues) or submit a PR.\n\n127 of 135 tracked features are implemented. See the [issue tracker](https:\u002F\u002Fgithub.com\u002Fproductdevbook\u002Fhucre\u002Fissues) 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\u002Fdecryption (AES-256, MS-OFFCRYPTO)\n- Threaded comments (Excel 365+) — synthesize from a fresh write (read + roundtrip already supported)\n- Checkboxes (Excel 2024+)\n- VBA\u002Fmacro injection\n- Slicers & 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:\u002F\u002Fgithub.com\u002FSheetJS\u002Fsheetjs)** — The most popular spreadsheet library. Feature-rich but large bundle (~300 KB), removed from npm (CDN-only), styling requires Pro license.\n- **[ExcelJS](https:\u002F\u002Fgithub.com\u002Fexceljs\u002Fexceljs)** — Read\u002Fwrite\u002Fstream XLSX with styling. Mature but has 12 dependencies (some with CVEs), CJS-only, no ESM.\n- **[xlsx-js-style](https:\u002F\u002Fgithub.com\u002Fgitbrent\u002Fxlsx-js-style)** — SheetJS fork that adds cell styling. Same bundle size and limitations as SheetJS.\n- **[xlsmith](https:\u002F\u002Fgithub.com\u002FChronicStone\u002Fxlsmith)** — 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:\u002F\u002Fgithub.com\u002Fdtjohnson\u002Fxlsx-populate)** — Template-based XLSX manipulation. Good for filling existing templates, limited write-from-scratch support.\n- **[better-xlsx](https:\u002F\u002Fgithub.com\u002Fnichenqin\u002Fbetter-xlsx)** — Lightweight XLSX writer with styling. Write-only, no read support.\n\n## License\n\n[MIT](.\u002FLICENSE) — Made by [productdevbook](https:\u002F\u002Fgithub.com\u002Fproductdevbook)\n","hucre 是一个无依赖的电子表格引擎，支持读写XLSX、CSV、ODS等多种格式。其核心功能包括零依赖设计、纯TypeScript编写以及在任何环境下均可运行的特点，还提供了流式处理和树摇优化等功能。特别适用于需要轻量级且高效处理电子表格数据的应用场景，如Web应用、Node.js服务等，能够满足从简单数据导入导出到复杂报表生成的需求。",2,"2026-06-11 03:51:49","high_star"]