@shbernal/ts-xlsx 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,14 +1,4 @@
1
1
  import { XlsxError } from '../errors.js';
2
- /**
3
- * Thrown when a `customUI` ribbon-customisation part (`customUI/customUI.xml` or
4
- * `customUI/customUI14.xml`) is present but cannot be parsed into a {@link CustomUiDocument} — malformed
5
- * XML, a missing or unrecognised `<customUI>` root namespace, or nesting deep enough to look hostile.
6
- * A workbook with no ribbon customisation never produces this: {@link Workbook.customUI} is an empty
7
- * array instead.
8
- *
9
- * The parser treats the part as hostile input (a spreadsheet library parses untrusted files), so a
10
- * malformed structure fails closed with this error rather than yielding a half-built tree.
11
- */
12
2
  export class CustomUiParseError extends XlsxError {
13
3
  name = 'CustomUiParseError';
14
4
  code = 'malformed-input';
@@ -1,7 +1,2 @@
1
- // Native `customUI` ribbon read: project a typed, read-only view of the ribbon tree (tabs → groups →
2
- // controls + callback names) over the `customUI.xml` / `customUI14.xml` parts a macro workbook carries.
3
- // Those parts are preserved byte-for-byte on round-trip (see `src/core/preserved.ts`); this is a
4
- // projection over them, the same posture as the VBA read view (`src/vba`). Authoring/editing the ribbon
5
- // is out of scope — round-trip fidelity already comes from verbatim preservation.
6
1
  export { CustomUiParseError } from './errors.js';
7
2
  export { CUSTOMUI_2006_NAMESPACE, CUSTOMUI_2007_REL_TYPE, CUSTOMUI_2009_NAMESPACE, CUSTOMUI_2010_REL_TYPE, isCustomUiRelType, parseCustomUi, } from './ribbon.js';
@@ -1,40 +1,13 @@
1
- // Turn a `customUI` ribbon-customisation part into a typed, read-only view of the ribbon tree.
2
- //
3
- // A `.xlsm` can carry two ribbon parts hung off the *package root* rels: `customUI/customUI.xml`
4
- // (Office 2007 RibbonX, namespace `.../2006/01/customui`) and `customUI/customUI14.xml` (Office 2010+,
5
- // namespace `.../2009/07/customui`, which adds backstage/QAT/commands). Both are preserved verbatim on
6
- // round-trip (see `src/core/preserved.ts`); this module adds a reader on top, mirroring how
7
- // `parseVbaProject` projects a read view over bytes the writer already round-trips opaquely.
8
- //
9
- // Scope (v1): the `<ribbon>` subtree only — tabs → groups → controls, plus each control's callback
10
- // names (the `onAction` a click invokes is the whole reason a macro workbook ships a ribbon). A
11
- // customUI document's `<commands>`, `<backstage>`, `<contextMenus>`, and the ribbon's `qat`/
12
- // `contextualTabs` are NOT parsed; they still round-trip byte-for-byte, they are just not surfaced here.
13
- //
14
- // Security posture: the part is untrusted input. The underlying `xmlEvents` scanner never expands
15
- // entities or DTDs (see `xml-read.ts`); on top of that this parser caps nesting depth and fails closed
16
- // with {@link CustomUiParseError} on any malformed or unrecognised structure rather than returning a
17
- // half-built tree.
18
1
  import { strFromU8 } from 'fflate';
19
2
  import { boolStrict, localName, xmlEvents } from '../xml/xml-read.js';
20
3
  import { CustomUiParseError } from './errors.js';
21
- /** The `customUI` root namespaces, one per {@link RibbonDialect}. */
22
4
  export const CUSTOMUI_2006_NAMESPACE = 'http://schemas.microsoft.com/office/2006/01/customui';
23
5
  export const CUSTOMUI_2009_NAMESPACE = 'http://schemas.microsoft.com/office/2009/07/customui';
24
- // The OPC relationship Type URIs Office wires the two ribbon parts under, from the package-root rels.
25
- // Both end `/ui/extensibility` (the 2010 one confusingly carries `2007` in its path); the reader
26
- // matches on that suffix — {@link isCustomUiRelType} — exactly as the rest of the reader matches
27
- // preserved relationship types by local suffix.
28
6
  export const CUSTOMUI_2007_REL_TYPE = 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility';
29
7
  export const CUSTOMUI_2010_REL_TYPE = 'http://schemas.microsoft.com/office/2007/relationships/ui/extensibility';
30
- /** Whether a package-root relationship Type URI points at a `customUI` ribbon part. */
31
8
  export function isCustomUiRelType(type) {
32
9
  return type.endsWith('/ui/extensibility');
33
10
  }
34
- // A hostile part could nest elements thousands deep; the recursive control mapper would then overflow
35
- // the call stack with an uncatchable RangeError. Capping tree depth keeps every failure a
36
- // CustomUiParseError. Real ribbons nest only a handful deep (tab > group > menu > submenu), so this
37
- // bound is unreachable by any legitimate document.
38
11
  const MAX_DEPTH = 256;
39
12
  const KNOWN_KINDS = new Set([
40
13
  'button',
@@ -56,12 +29,6 @@ const KNOWN_KINDS = new Set([
56
29
  'control',
57
30
  'item',
58
31
  ]);
59
- /**
60
- * Parse a `customUI` part (raw UTF-8 bytes or its decoded text) into a {@link CustomUiDocument}.
61
- *
62
- * @throws {CustomUiParseError} if the XML is malformed, the root is not a `<customUI>` element in
63
- * a recognised namespace, or the tree nests beyond {@link MAX_DEPTH}.
64
- */
65
32
  export function parseCustomUi(input) {
66
33
  const xml = typeof input === 'string' ? input : strFromU8(input);
67
34
  let root;
@@ -82,10 +49,6 @@ export function parseCustomUi(input) {
82
49
  const ribbonEl = findChild(customUi, 'ribbon');
83
50
  return ribbonEl === undefined ? { dialect } : { dialect, ribbon: toRibbon(ribbonEl) };
84
51
  }
85
- // Resolve the namespace bound to the customUI element's own name and map it to a dialect. A default
86
- // namespace (`xmlns=`) governs an unprefixed `<customUI>`; a prefixed `<p:customUI>` is governed by
87
- // that prefix's `xmlns:p`. Neither of the two known namespaces present means this is not a customUI part
88
- // this reader understands — fail closed rather than guess.
89
52
  function dialectOf(customUi) {
90
53
  const colon = customUi.name.indexOf(':');
91
54
  const nsAttr = colon === -1 ? 'xmlns' : `xmlns:${customUi.name.slice(0, colon)}`;
@@ -127,8 +90,6 @@ function toControl(el) {
127
90
  ...(el.children.length > 0 ? { children: el.children.map((child) => toControl(child)) } : {}),
128
91
  };
129
92
  }
130
- // The id/label attributes lifted onto every tab, group, and control. Emitted only when present, so an
131
- // absent attribute stays absent (exactOptionalPropertyTypes) rather than becoming an explicit undefined.
132
93
  function identity(attrs) {
133
94
  return {
134
95
  ...(attrs.id !== undefined ? { id: attrs.id } : {}),
@@ -137,10 +98,6 @@ function identity(attrs) {
137
98
  ...(attrs.label !== undefined ? { label: attrs.label } : {}),
138
99
  };
139
100
  }
140
- // Build the element tree from the SAX event stream with an explicit stack (no recursion), capping depth
141
- // so a hostile part cannot force the later recursive walk to overflow. Text and comments carry no ribbon
142
- // meaning and are ignored. Throws CustomUiParseError on unbalanced markup; a lower-level SyntaxError from
143
- // the scanner is caught and re-wrapped by the caller.
144
101
  function buildTree(xml) {
145
102
  const root = { name: '#root', local: '#root', attrs: {}, children: [] };
146
103
  const stack = [root];
@@ -7,6 +7,7 @@ export type { Comment, CommentThread, Mention, Person } from '../core/comment-th
7
7
  export type { CfValueObject, ConditionalFormatting, ConditionalFormattingRule, } from '../core/conditional-formatting.ts';
8
8
  export type { DataValidation, DataValidationEntry, DataValidationErrorStyle, DataValidationOperator, DataValidationType, } from '../core/data-validation.ts';
9
9
  export { type AnchoredImage, type AnchorPoint, type Extent, type ImageAnchor, type ImageEditAs, isOneCellAnchor, type OneCellAnchor, PX_TO_EMU, type TwoCellAnchor, type WorkbookImage, } from '../core/image.ts';
10
+ export { MAX_COLUMN_WIDTH, MAX_ROW_HEIGHT } from '../core/limits.ts';
10
11
  export type { HeaderFooter, PageBreak, PageMargins, PageSetup, PrintOptions, } from '../core/page-setup.ts';
11
12
  export { type ParsedPivotField, type ParsedPivotSource, type ParsedPivotTable, type PivotCacheField, type PivotItem, type PivotMetric, type PivotNumericSummary, type PivotRecordCell, type PivotSourceKind, PivotTable, type PivotTableOptions, } from '../core/pivot-table.ts';
12
13
  export type { PreservedPart, PreservedRelationship, PreservedRootReference, PreservedWorksheetReference, } from '../core/preserved.ts';
@@ -16,8 +17,9 @@ export { Row } from '../core/row.ts';
16
17
  export type { Alignment, Border, BorderEdge, BorderStyle, CellStyle, Color, DifferentialStyle, Fill, FillPatternType, Font, FontScheme, FontVerticalAlignment, GradientFill, GradientStop, HorizontalAlignment, NamedCellStyle, PatternFill, Protection, TableStyleNamespace, TableStyleTable, UnderlineStyle, VerticalAlignment, } from '../core/style.ts';
17
18
  export { Table, type TableColumn, type TableColumnStyle, type TableOptions, type TableRegion, type TableStyleInfo, } from '../core/table.ts';
18
19
  export { isTableStyleElementType, STRIPE_ELEMENT_TYPES, TABLE_STYLE_ELEMENT_TYPES, type TableStyle, type TableStyleElement, type TableStyleElementType, } from '../core/table-style.ts';
20
+ export { estimateWrappedLines } from '../core/text-metrics.ts';
19
21
  export { DEFAULT_THEME_COLOR_SCHEME, DEFAULT_THEME_FONTS, parseThemeColorScheme, THEME_COLOR_SLOTS, type ThemeColorScheme, type ThemeColorSlot, type ThemeFontScheme, type ThemeOverrides, } from '../core/theme.ts';
20
- export { type CellValue, coerceCellValue, type DataTableFormulaValue, detectValueType, ERROR_CODES, type ErrorCode, type ErrorValue, type FormulaResult, type FormulaValue, type HyperlinkValue, isErrorCode, type RichTextRun, type RichTextValue, richTextToPlain, type SharedFormulaValue, ValueType, } from '../core/value.ts';
22
+ export { type CellValue, cellValueToText, coerceCellValue, type DataTableFormulaValue, detectValueType, ERROR_CODES, type ErrorCode, type ErrorValue, type FormulaResult, type FormulaValue, type HyperlinkValue, isDataTableFormulaValue, isErrorCode, isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, type RichTextRun, type RichTextValue, richTextToPlain, type SharedFormulaValue, ValueType, } from '../core/value.ts';
21
23
  export { type AddImageOptions, type AddWorksheetOptions, DEFAULT_WORKBOOK_VIEW, type DefinedName, type PreservedWorkbookReference, Workbook, type WorkbookProperties, type WorkbookView, } from '../core/workbook.ts';
22
24
  export type { WorkbookProtection, WorkbookProtectionCredentialAttr, } from '../core/workbook-protection.ts';
23
25
  export { type CellModel, type ColumnProperties, type OutlineProperties, type RowInput, type RowProperties, type SheetView, Worksheet, type WorksheetModel, type WorksheetProperties, type WorksheetState, } from '../core/worksheet.ts';
@@ -1,20 +1,16 @@
1
- // Entry barrel for `@shbernal/ts-xlsx/core` — the document model, with no serialisation attached.
2
- //
3
- // This is what a consumer imports to build or inspect a workbook in memory: the classes, the
4
- // property shapes, and the value vocabulary. It pulls in no ZIP, no XML writer and no BIFF12
5
- // decoder. Errors are not here — the whole failure taxonomy lives behind `/errors`, so that a
6
- // caller who only needs to branch on a failure never loads a codec to get the class.
7
1
  export { columnToNumber, decodeAddress, decodeRange, encodeAddress, MAX_COLUMN, MAX_ROW, numberToColumn, } from '../core/address.js';
8
2
  export { Cell } from '../core/cell.js';
9
3
  export { applyTint, DEFAULT_INDEXED_COLORS, resolveColor, SYSTEM_INDEXED_COLORS, } from '../core/color-resolution.js';
10
4
  export { Column } from '../core/column.js';
11
5
  export { isOneCellAnchor, PX_TO_EMU, } from '../core/image.js';
6
+ export { MAX_COLUMN_WIDTH, MAX_ROW_HEIGHT } from '../core/limits.js';
12
7
  export { PivotTable, } from '../core/pivot-table.js';
13
8
  export { Range } from '../core/range.js';
14
9
  export { Row } from '../core/row.js';
15
10
  export { Table, } from '../core/table.js';
16
11
  export { isTableStyleElementType, STRIPE_ELEMENT_TYPES, TABLE_STYLE_ELEMENT_TYPES, } from '../core/table-style.js';
12
+ export { estimateWrappedLines } from '../core/text-metrics.js';
17
13
  export { DEFAULT_THEME_COLOR_SCHEME, DEFAULT_THEME_FONTS, parseThemeColorScheme, THEME_COLOR_SLOTS, } from '../core/theme.js';
18
- export { coerceCellValue, detectValueType, ERROR_CODES, isErrorCode, richTextToPlain, ValueType, } from '../core/value.js';
14
+ export { cellValueToText, coerceCellValue, detectValueType, ERROR_CODES, isDataTableFormulaValue, isErrorCode, isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, richTextToPlain, ValueType, } from '../core/value.js';
19
15
  export { DEFAULT_WORKBOOK_VIEW, Workbook, } from '../core/workbook.js';
20
16
  export { Worksheet, } from '../core/worksheet.js';
@@ -1,6 +1,2 @@
1
- // Entry barrel for `@shbernal/ts-xlsx/csv` — delimited text in and out.
2
- //
3
- // The two directions are asymmetric in what they cost: writing needs only a worksheet and the
4
- // value vocabulary, while reading builds a `Workbook` and therefore pulls the model in whole.
5
1
  export { readCsv } from '../io/csv/read.js';
6
2
  export { writeCsv, writeCsvText } from '../io/csv/write.js';
@@ -1,3 +1 @@
1
- // Entry barrel for `@shbernal/ts-xlsx/customui` — the read-only ribbon view over a macro
2
- // workbook's preserved `customUI` parts.
3
1
  export { CUSTOMUI_2006_NAMESPACE, CUSTOMUI_2007_REL_TYPE, CUSTOMUI_2009_NAMESPACE, CUSTOMUI_2010_REL_TYPE, isCustomUiRelType, parseCustomUi, } from '../customui/ribbon.js';
@@ -1,16 +1,3 @@
1
- // Entry barrel for `@shbernal/ts-xlsx/errors` — the whole failure taxonomy, and nothing else.
2
- //
3
- // Every error class the library throws is exported here and from no other entry. That rule is
4
- // what keeps the entry barrels disjoint (a container-level failure belongs to no single codec:
5
- // `readXlsx` and `readXlsb` both raise `UnsupportedFormatError`), and it makes the answer to
6
- // "what can this throw at me?" one import rather than a hunt across codecs.
7
- //
8
- // It is also the cheapest thing in the package by an order of magnitude: the classes reach
9
- // nothing but each other, so a service that only needs to classify a failure — log it, map it to
10
- // an HTTP status, decide whether to retry — pays for the taxonomy and not for a parser.
11
- //
12
- // `XlsxError` is the one-line answer to "was that us?"; `XlsxErrorCode` is the kind of failure,
13
- // shared across classes on purpose (see `src/errors.ts`).
14
1
  export { CustomUiParseError } from '../customui/errors.js';
15
2
  export { AuthoringError, InternalError, XlsxError } from '../errors.js';
16
3
  export { PackageReadError, UnsupportedFormatError, } from '../io/opc/errors.js';
@@ -1,7 +1,2 @@
1
- // Entry barrel for `@shbernal/ts-xlsx/vba` — the native VBA read view and the structural edits.
2
- //
3
- // A deliberately narrower face than `src/vba/index.ts`, which is the *internal* barrel and also
4
- // carries the CFB writer, the MS-OVBA container primitives and the part-path constants that
5
- // `Workbook` and the codecs need. Those are implementation, not API.
6
1
  export { parseVbaProject, } from '../vba/project.js';
7
2
  export { addVbaReference, removeVbaModule, } from '../vba/project-editor.js';
@@ -1,7 +1 @@
1
- // Entry barrel for `@shbernal/ts-xlsx/xlsb` — the BIFF12 reader.
2
- //
3
- // `readXlsx` auto-detects and reads a binary `.xlsb` too, so most callers never name this; it is
4
- // public for the caller that holds bytes it already knows to be `.xlsb` and wants to say so. That
5
- // dispatch also means this entry is not the way to *avoid* the BIFF12 codec — importing
6
- // `/xlsx` loads it either way (see the per-entry budgets in `scripts/size-budget.ts`).
7
1
  export { readXlsb } from '../io/xlsb/read.js';
@@ -1,16 +1,3 @@
1
- // Entry barrel for `@shbernal/ts-xlsx/xlsx` — the XML codec: whole-workbook read and write, the
2
- // streaming pair, and the in-place VBA part edits.
3
- //
4
- // The streaming reader's entry points are public; the granular per-row/cell/sheet output shapes
5
- // (`StreamedRow`/`StreamedCell`/`StreamedSheet` as *read*) are intentionally left as inferred
6
- // structural types rather than named commitments while that surface settles. The streaming
7
- // *writer*'s whole surface is named: its incremental workbook/worksheet/row handles are classes
8
- // and their options are interfaces, so there is nothing structural left un-named. A styled row
9
- // hands back `Cell`, which belongs to `/core`.
10
- //
11
- // Streaming is not its own entry point. Measured, `read-rows` + `write-stream` reach every module
12
- // `read` + `write` do plus three — an entry that costs what the codec costs is an alias, not a
13
- // packaging boundary.
14
1
  export { editXlsxVbaAddReference, editXlsxVbaRemoveModule } from '../io/xlsx/edit-vba.js';
15
2
  export { readXlsx } from '../io/xlsx/read.js';
16
3
  export { readSheetRows, readWorkbookStream, } from '../io/xlsx/read-rows.js';
package/dist/errors.d.ts CHANGED
@@ -14,6 +14,12 @@
14
14
  * unreachable exhaustiveness guard (so: `'internal'`), and the one genuine feature gap — a binary
15
15
  * `.xlsb` cannot be row-streamed — is already reported by {@link UnsupportedFormatError}'s `format`
16
16
  * branch. A code with no throw site would be a promise the library does not keep.
17
+ *
18
+ * **Which of these is worth reporting upstream.** `'internal'` always is, and says so at runtime.
19
+ * `'unsupported-format'` and `'malformed-input'` are worth reporting when the file in hand opens
20
+ * cleanly in Excel — that combination means we are the ones who cannot read it, which is a gap, not
21
+ * a corrupt input. `'authoring'` is worth reporting only if the document it refused is one a real
22
+ * workbook can express. See `skills/ts-xlsx-upstream` for how to file one.
17
23
  */
18
24
  export type XlsxErrorCode = 'unsupported-format' | 'malformed-input' | 'authoring' | 'internal';
19
25
  /**
@@ -58,8 +64,16 @@ export declare class AuthoringError extends XlsxError {
58
64
  * No caller can provoke one, so it is not a failure mode to handle: seeing it means the bug is ours.
59
65
  * It exists as a distinct type so that "unreachable" is *stated* rather than implied by a bare
60
66
  * `Error`, which reads identically to a throw nobody has classified yet.
67
+ *
68
+ * It is the one class in the taxonomy that rewrites its own message, appending {@link REPORT_NOTICE}
69
+ * below the invariant that broke. The constructor is where that lives so a throw site added later
70
+ * inherits it — the alternative, a notice pasted at each of the throw sites, is one every future
71
+ * site can forget. Every other class leaves `message` exactly as given: `'malformed-input'` is a
72
+ * routine outcome for a library that reads untrusted files, and a "report this" banner on each
73
+ * corrupt input would train callers to ignore the one banner that always means something.
61
74
  */
62
75
  export declare class InternalError extends XlsxError {
63
76
  readonly name = "InternalError";
64
77
  readonly code = "internal";
78
+ constructor(message?: string, options?: ErrorOptions);
65
79
  }
package/dist/errors.js CHANGED
@@ -1,66 +1,15 @@
1
- // The failure taxonomy every layer throws through.
2
- //
3
- // Typed error classes grew up one per subsystem that happened to want one, sharing no ancestor, so a
4
- // caller could not ask "did *this library* fail?" without naming all of them — while the model's own
5
- // validation threw bare `Error`, distinguishable only by string-matching the message. For a library
6
- // whose input is untrusted files, "was it my file or my call?" is a first-class question, and it had
7
- // no answer.
8
- //
9
- // Two levels of branch, deliberately non-redundant:
10
- // - `code` says what *kind* of failure this is — the question a caller usually has.
11
- // - `name` (and `instanceof`) says exactly which one — the question a caller occasionally has.
12
- // A code shared by several classes is the design, not an oversight: a `code` in 1:1 correspondence
13
- // with the classes would carry nothing the class did not already carry.
14
- //
15
- // Scalar argument validation stays *outside* this taxonomy — see {@link AuthoringError} for where the
16
- // line falls and why.
17
- //
18
- // This module sits below every layer that throws (`src/xml`, `src/core`, `src/io`, `src/vba`,
19
- // `src/customui` all import it), so it imports nothing itself; `scripts/check-layering.ts` enforces
20
- // that.
21
- /**
22
- * The common ancestor of every error this library raises deliberately. `catch (e) { if (e instanceof
23
- * XlsxError) … }` is the one-line answer to "was that us?", and {@link code} then says which kind of
24
- * failure it was without naming a single subclass.
25
- *
26
- * Abstract on purpose: a failure always has a kind, so there is never a reason to throw the base.
27
- * Every subclass fixes {@link code} to a literal, which makes the class hierarchy a discriminated
28
- * union — narrowing on `error.code` narrows the type.
29
- *
30
- * The constructor is inherited from `Error`, so every subclass accepts `{cause}`; layers that wrap a
31
- * lower-level failure are expected to pass it rather than flatten it into the message.
32
- */
33
1
  export class XlsxError extends Error {
34
2
  }
35
- /**
36
- * Thrown when the caller describes a document that cannot exist: a pivot table with no row field, a
37
- * table whose columns do not span its range, a merge that overlaps another, a workbook with no
38
- * worksheets. The document model, not a single argument, is what is wrong.
39
- *
40
- * **Where the line falls against native errors.** A single scalar that is out of range, unparseable,
41
- * or the wrong type stays a native `RangeError` / `SyntaxError` / `TypeError` — those types exist for
42
- * exactly that, every caller already reads them, and wrapping them would make this taxonomy a
43
- * re-implementation of the language's. `AuthoringError` starts where a *composite* is internally
44
- * inconsistent, or contradicts something a workbook can express. `getColumn(0)` is a `RangeError`;
45
- * a table that names a column twice is an `AuthoringError`.
46
- *
47
- * It is always the calling code that is wrong, never the input file — a malformed file raises a
48
- * `'malformed-input'` error instead.
49
- */
50
3
  export class AuthoringError extends XlsxError {
51
4
  name = 'AuthoringError';
52
5
  code = 'authoring';
53
6
  }
54
- /**
55
- * Thrown where an invariant the library itself maintains turns out not to hold — typically an index
56
- * that a preceding pass proved to be in range, re-checked because `noUncheckedIndexedAccess` makes
57
- * the possibility of `undefined` explicit and casting it away would be worse.
58
- *
59
- * No caller can provoke one, so it is not a failure mode to handle: seeing it means the bug is ours.
60
- * It exists as a distinct type so that "unreachable" is *stated* rather than implied by a bare
61
- * `Error`, which reads identically to a throw nobody has classified yet.
62
- */
7
+ const REPORT_NOTICE = 'This is a bug in ts-xlsx, not in your file or your code. Please report it:\n' +
8
+ 'https://github.com/shbernal/ts-xlsx/issues/new?template=agent-report.yml&labels=agent-reported';
63
9
  export class InternalError extends XlsxError {
64
10
  name = 'InternalError';
65
11
  code = 'internal';
12
+ constructor(message, options) {
13
+ super(message === undefined ? REPORT_NOTICE : `${message}\n\n${REPORT_NOTICE}`, options);
14
+ }
66
15
  }
package/dist/index.js CHANGED
@@ -1,15 +1,3 @@
1
- // The convenience entry point: everything the package exports, under one specifier.
2
- //
3
- // It is a union of the subpath entry barrels in `src/entries/`, which are the real public faces —
4
- // `@shbernal/ts-xlsx/core`, `/xlsx`, `/xlsb`, `/csv`, `/vba`, `/customui`, `/errors`. Each symbol
5
- // is listed in exactly one of them, so there is no second list to keep in step here and a star
6
- // re-export cannot silently drop a name to an ambiguity. `scripts/check-layering.ts` holds the
7
- // entries disjoint and keeps this file's composition honest.
8
- //
9
- // Importing from here is the right default; it costs nothing extra to a bundler, because
10
- // `sideEffects: false` lets an unused module be dropped whole. Reach for a subpath when the
11
- // consumer has no bundler to do that for it, or when you want the module graph itself to say
12
- // which half of the library a service depends on.
13
1
  export * from './entries/core.js';
14
2
  export * from './entries/csv.js';
15
3
  export * from './entries/customui.js';
@@ -1,19 +1,5 @@
1
- // CSV parsing — flat delimited text back into a one-sheet workbook.
2
- //
3
- // The hard part of reading CSV is not splitting fields; it is deciding a field's *type* without
4
- // corrupting data. The rules here are deliberate and lossless-by-default:
5
- // - An empty field is the empty cell (`null`); a whitespace-only field is a string, never the
6
- // number 0 that `Number(" ")` would silently produce.
7
- // - A numeric-looking field becomes a number only when its magnitude is exactly representable
8
- // (within the safe-integer range); an oversized id like a 20-digit account number is kept as
9
- // its original string so no digits are lost.
10
- // - Only a strictly-formatted ISO date (`YYYY-MM-DD`, optional time) becomes a Date; padded ids
11
- // and dash-codes such as `2020-00001` or `1-3` stay strings.
12
- // A caller can override coercion wholesale with `map` (e.g. the identity function to keep every
13
- // field a raw string, preserving leading zeros).
14
1
  import { Workbook } from '../../core/workbook.js';
15
2
  import { AuthoringError } from '../../errors.js';
16
- /** Parse CSV text (or UTF-8 bytes) into a workbook holding a single worksheet. */
17
3
  export function readCsv(input, options = {}) {
18
4
  const text = stripBom(typeof input === 'string' ? input : Buffer.from(input).toString('utf8'));
19
5
  const delimiter = options.delimiter ?? ',';
@@ -32,9 +18,6 @@ export function readCsv(input, options = {}) {
32
18
  function stripBom(text) {
33
19
  return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
34
20
  }
35
- // A character-scan parser: quotes toggle literal mode, a doubled quote inside a quoted field is one
36
- // quote, and a row ends on LF (a preceding CR is dropped, so CRLF and LF read the same). A final
37
- // trailing newline does not yield a spurious empty row.
38
21
  function parseCsvRows(text, delimiter) {
39
22
  const rows = [];
40
23
  let row = [];
@@ -87,7 +70,6 @@ function defaultCsvCoerce(field) {
87
70
  return null;
88
71
  if (NUMERIC.test(field)) {
89
72
  const value = Number(field);
90
- // Beyond the safe-integer range a double silently loses digits; keep the original text instead.
91
73
  return Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER ? value : field;
92
74
  }
93
75
  const iso = ISO_DATE.exec(field);
@@ -1,18 +1,6 @@
1
- // CSV serialization the flat-text sibling of the XLSX writer.
2
- //
3
- // A worksheet is a rectangle of typed cells; CSV is that rectangle flattened to delimited
4
- // text. The lossy direction (styles, formulas-as-formulas, multiple sheets) is inherent to the
5
- // format, so this writer makes the honest choices explicit: one selected sheet, each row sized to
6
- // its own populated extent (never clamped to a sibling row's width), a formula rendered as its
7
- // cached result, a Date rendered by a caller-supplied format or a full ISO-8601 timestamp.
8
- //
9
- // `writeCsvText` yields the logical text; `writeCsv` encodes it to bytes and — for UTF-8, the
10
- // default — prepends a byte-order mark so a consumer such as Excel detects the encoding and does
11
- // not mangle non-ASCII on open. The BOM is a byte-level marker, not part of the logical text.
12
- import { isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, richTextToPlain, } from '../../core/value.js';
1
+ import { cellValueToText, isDataTableFormulaValue, isFormulaValue, isSharedFormulaValue, } from '../../core/value.js';
13
2
  import { AuthoringError } from '../../errors.js';
14
3
  const UTF8_BOM = Uint8Array.of(0xef, 0xbb, 0xbf);
15
- /** The logical CSV text of one worksheet — no BOM, no byte encoding. */
16
4
  export function writeCsvText(workbook, options = {}) {
17
5
  const sheet = selectSheet(workbook, options.sheetName);
18
6
  const delimiter = options.delimiter ?? ',';
@@ -36,7 +24,6 @@ export function writeCsvText(workbook, options = {}) {
36
24
  }
37
25
  return lines.join(rowDelimiter);
38
26
  }
39
- /** The CSV bytes of one worksheet in the requested encoding, with a UTF-8 BOM by default. */
40
27
  export function writeCsv(workbook, options = {}) {
41
28
  const text = writeCsvText(workbook, options);
42
29
  const encoding = options.encoding ?? 'utf8';
@@ -56,33 +43,16 @@ function selectSheet(workbook, name) {
56
43
  throw new AuthoringError('workbook has no worksheet to write as CSV');
57
44
  return first;
58
45
  }
59
- const sheet = workbook.getWorksheet(name);
60
- if (sheet === undefined)
61
- throw new AuthoringError(`no worksheet named "${name}" to write as CSV`);
62
- return sheet;
46
+ return workbook.requireWorksheet(name);
63
47
  }
64
48
  function csvFieldText(value, options) {
65
- if (value === null)
66
- return '';
67
- if (typeof value === 'number')
68
- return String(value);
69
- if (typeof value === 'boolean')
70
- return value ? 'TRUE' : 'FALSE';
71
- if (typeof value === 'string')
72
- return value;
73
- if (value instanceof Date)
49
+ if (value instanceof Date && options.dateFormat !== undefined) {
74
50
  return formatDate(value, options.dateFormat, options.dateUTC ?? false);
75
- if (isErrorValue(value))
76
- return value.error;
77
- if (isRichTextValue(value))
78
- return richTextToPlain(value);
79
- if (isHyperlinkValue(value)) {
80
- return typeof value.text === 'string' ? value.text : richTextToPlain(value.text);
81
51
  }
82
- if (isFormulaValue(value) || isSharedFormulaValue(value)) {
52
+ if (isFormulaValue(value) || isSharedFormulaValue(value) || isDataTableFormulaValue(value)) {
83
53
  return value.result === undefined ? '' : csvFieldText(value.result, options);
84
54
  }
85
- return '';
55
+ return cellValueToText(value);
86
56
  }
87
57
  const DATE_TOKENS = /YYYY|YY|MM|DD|HH|mm|ss|M|D|H|m|s/g;
88
58
  function formatDate(date, format, utc) {
@@ -1,25 +1,9 @@
1
- // Typed errors the reader raises when it is handed something that is not a readable OOXML `.xlsx`
2
- // package. A spreadsheet library parses untrusted files, so the failure a caller sees must be a clear,
3
- // programmatically-branchable signal — not a raw zip-internals string (which is opaque, and can leak an
4
- // absolute filesystem path from the layer below). See the spec
5
- // `docs/knowledge/specs/unsupported-input-format-typed-error.md`.
6
1
  import { XlsxError } from '../../errors.js';
7
2
  const DEFAULT_MESSAGE = {
8
3
  xls: 'the legacy .xls binary format (BIFF/OLE2 compound file) is not supported; only OOXML .xlsx is read',
9
4
  xlsb: 'this entry point does not read the binary .xlsb format (BIFF12)',
10
5
  unknown: 'not a valid .xlsx package: no OOXML workbook part was found',
11
6
  };
12
- /**
13
- * Thrown when input is not a readable `.xlsx` package. The single {@link format} field is the branch a
14
- * caller keys on (rather than a subclass per format), so a `catch` can distinguish a legacy `.xls`, a
15
- * binary `.xlsb`, and an unrecognised blob without string-matching the message.
16
- *
17
- * The message never carries a filesystem path or the underlying zip library's internals — the whole
18
- * point of the type is that the classification, not a leaked lower-layer string, is what the caller sees.
19
- *
20
- * {@link format} stays the branch for *which* unsupported input this was; the inherited
21
- * {@link XlsxError.code} answers the coarser question of what kind of failure it is.
22
- */
23
7
  export class UnsupportedFormatError extends XlsxError {
24
8
  name = 'UnsupportedFormatError';
25
9
  code = 'unsupported-format';
@@ -29,20 +13,6 @@ export class UnsupportedFormatError extends XlsxError {
29
13
  this.format = format;
30
14
  }
31
15
  }
32
- /**
33
- * Thrown when the input *is* a ZIP container but it cannot be unpacked: the archive is corrupt or
34
- * truncated, or inflating it would push total uncompressed output past the caller's bound (which is
35
- * how a zip bomb presents).
36
- *
37
- * The neighbouring {@link UnsupportedFormatError} says the input is a different *kind* of thing; this
38
- * one says it is the right kind and we will not (or cannot) unpack it. Keeping them apart is what
39
- * lets a caller answer "should I try another reader, or reject this file?" — and it is what replaced
40
- * the message-prefix match the bomb refusal used to be recognised by.
41
- *
42
- * The zip library's own failure text never survives into either the message or `cause`: it can name
43
- * internals — or an absolute filesystem path — from the layer below, and this type carries the
44
- * classification precisely so no lower-layer string has to.
45
- */
46
16
  export class PackageReadError extends XlsxError {
47
17
  name = 'PackageReadError';
48
18
  code = 'malformed-input';
@@ -1,38 +1,6 @@
1
- // Bounded, streaming inflation of an OPC (`.xlsx`) zip package.
2
- //
3
- // Decompression is the reader's first hostile-input surface: a "zip bomb" ships a few
4
- // kilobytes that inflate to gigabytes. The zip's own size headers cannot be trusted to
5
- // bound this — they are attacker-controlled. A header that declares a *large* size makes a
6
- // naïve reader preallocate that much (an amplifier: tiny input, huge allocation); a header
7
- // that lies *small* makes a size-preallocating inflater silently truncate real data. So we
8
- // consult the declared sizes for nothing. Instead the compressed archive is fed to fflate's
9
- // streaming unzip in slices, the decompressor grows its output from the bytes it actually
10
- // produces, and a running counter of real output aborts the moment it crosses the cap.
11
- //
12
- // Because DEFLATE cannot expand input by more than ~1032:1, feeding at most INPUT_SLICE
13
- // compressed bytes before each counter check bounds the worst-case overshoot past the cap
14
- // to one slice's expansion — not the whole (possibly enormous) stream.
15
1
  import { Unzip, UnzipInflate } from 'fflate';
16
2
  import { PackageReadError } from './errors.js';
17
- // Compressed input is pushed in slices this size so decompressed output arrives in
18
- // increments the running counter can check. Small enough that the worst-case overshoot
19
- // (slice × DEFLATE's ~1032:1 ceiling ≈ 16 MiB) is negligible against a sane cap; large
20
- // enough that a legitimate multi-hundred-megabyte package still streams in cheaply.
21
3
  const INPUT_SLICE = 1 << 14;
22
- /**
23
- * Inflate every part of an `.xlsx` zip package, enforcing a hard ceiling on total
24
- * decompressed output.
25
- *
26
- * @param data The raw zip bytes.
27
- * @param cap Maximum total uncompressed output, in bytes, across all parts. Enforced
28
- * against bytes actually produced, never against the archive's declared sizes.
29
- * @returns A map of part path to inflated bytes.
30
- * @throws {PackageReadError} if inflation would exceed `cap` — a probable zip bomb.
31
- * @throws {Error} raised by the zip layer if the archive is malformed or a part uses an unsupported
32
- * compression method. Callers reach this through {@link inflateSpreadsheetPackage}, which is where
33
- * those are classified; the raw error is deliberately not re-typed here, because its text is the
34
- * one thing that must not reach a caller unfiltered.
35
- */
36
4
  export function inflatePackage(data, cap) {
37
5
  const files = {};
38
6
  let total = 0;
@@ -57,9 +25,6 @@ export function inflatePackage(data, cap) {
57
25
  if (final)
58
26
  files[file.name] = join(chunks, size);
59
27
  };
60
- // `start()` reports an unsupported compression method through `ondata` above, then
61
- // throws trying to build the missing decoder. Keep the reported error (its message
62
- // names the method) and swallow the raw follow-on throw.
63
28
  try {
64
29
  file.start();
65
30
  }
@@ -1,18 +1,3 @@
1
- /**
2
- * The namespace URIs the Open Packaging Conventions layer itself owns — the ones that describe a
3
- * *package* rather than the spreadsheet inside it. Every OOXML package carries these regardless of
4
- * which serialisation its office document uses, so an `.xlsb` and an `.xlsx` agree on them exactly.
5
- *
6
- * The SpreadsheetML vocabulary (the main namespace, DrawingML, the Microsoft extension URIs) is a
7
- * property of the XML codec and lives in `../xlsx/namespaces.ts`.
8
- */
9
- /** The `xmlns` of every `.rels` package relationships part. */
10
1
  export const PKG_RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
11
- /** The `xmlns` of the package's `[Content_Types].xml`. */
12
2
  export const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
13
- /**
14
- * Scopes both the relationship *type* URIs a `.rels` part declares and the `r:id` references a part's
15
- * body carries. Part-level rather than package-level in ECMA-376's own split, but it is the vocabulary
16
- * the relationship graph is written in, so it belongs with the graph and not with either codec.
17
- */
18
3
  export const RELATIONSHIPS_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
@@ -1,26 +1,15 @@
1
- // The path algebra of an OPC package: a part's extension, where its relationships live, and how one
2
- // part names another. Pure string work over package-absolute paths, with no notion of what any part
3
- // contains — so the XML and BIFF12 codecs, and the readers and writers within each, share it.
4
- // Where the writer always puts the theme part. The workbook's theme relationship and the package's
5
- // content-type override both name this path unconditionally, so a theme preserved from a source
6
- // package that called its part something else is re-emitted here rather than at its original name.
7
1
  export const THEME_PART_PATH = 'xl/theme/theme1.xml';
8
- // The extension of a part path (`xl/media/image1.jpeg` → `jpeg`), or '' when it carries none.
9
2
  export function extensionOf(partPath) {
10
3
  const dot = partPath.lastIndexOf('.');
11
4
  const slash = partPath.lastIndexOf('/');
12
5
  return dot > slash ? partPath.slice(dot + 1) : '';
13
6
  }
14
- // The relationships part path for `dir/name.ext` → `dir/_rels/name.ext.rels`.
15
7
  export function relsPathFor(partPath) {
16
8
  const slash = partPath.lastIndexOf('/');
17
9
  const dir = slash === -1 ? '' : partPath.slice(0, slash + 1);
18
10
  const base = slash === -1 ? partPath : partPath.slice(slash + 1);
19
11
  return `${dir}_rels/${base}.rels`;
20
12
  }
21
- // A relationship target expressed relative to the part that carries it: the `..` hops out of the
22
- // referencing part's directory up to the common ancestor, then down to the target. Both paths are
23
- // package-absolute (`xl/drawings/preservedP1.vml` → `xl/media/preservedP2.jpeg` → `../media/preservedP2.jpeg`).
24
13
  export function relativePartPath(fromPath, toPath) {
25
14
  const fromDir = fromPath.split('/').slice(0, -1);
26
15
  const toSegments = toPath.split('/');