@shbernal/ts-xlsx 1.0.3 → 1.1.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.
@@ -22,6 +22,16 @@ export declare class Cell {
22
22
  set value(value: CellValue | undefined);
23
23
  /** The observable {@link ValueType} of the current value. */
24
24
  get type(): ValueType;
25
+ /**
26
+ * The cell's value as plain text ({@link cellValueToText}), `""` when it is empty — so a reader
27
+ * that only wants strings never has to narrow the value union itself.
28
+ *
29
+ * Read-only, because text is a *rendering* of the value and not a second place to store one:
30
+ * writing `"3"` here could only mean the string `"3"`, which is exactly `value = '3'` and reads
31
+ * nothing like it. The number format is not applied either — the style is not the cell's value,
32
+ * so a currency cell's text carries no currency sign.
33
+ */
34
+ get text(): string;
25
35
  /**
26
36
  * Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
27
37
  * changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
package/dist/core/cell.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import { encodeAddress } from './address.js';
8
8
  import { NAMED_STYLE_ID } from './internal.js';
9
9
  import { assignStyleFacets, } from './style.js';
10
- import { coerceCellValue, detectValueType, } from './value.js';
10
+ import { cellValueToText, coerceCellValue, detectValueType, } from './value.js';
11
11
  /**
12
12
  * A single cell owns its value and every style facet outright. Each facet below — fill, number format,
13
13
  * font, border, alignment, protection, quote-prefix, and note — is held in the cell's own field and
@@ -54,6 +54,18 @@ export class Cell {
54
54
  get type() {
55
55
  return detectValueType(this.#value);
56
56
  }
57
+ /**
58
+ * The cell's value as plain text ({@link cellValueToText}), `""` when it is empty — so a reader
59
+ * that only wants strings never has to narrow the value union itself.
60
+ *
61
+ * Read-only, because text is a *rendering* of the value and not a second place to store one:
62
+ * writing `"3"` here could only mean the string `"3"`, which is exactly `value = '3'` and reads
63
+ * nothing like it. The number format is not applied either — the style is not the cell's value,
64
+ * so a currency cell's text carries no currency sign.
65
+ */
66
+ get text() {
67
+ return cellValueToText(this.#value);
68
+ }
57
69
  /**
58
70
  * Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
59
71
  * changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
@@ -23,7 +23,15 @@ export declare class Column {
23
23
  */
24
24
  get key(): string | undefined;
25
25
  set key(key: string | undefined);
26
- /** Column width in character units; `undefined` leaves the sheet default in force. */
26
+ /**
27
+ * Column width in character units — digits of the workbook default font's maximum digit width,
28
+ * so what one unit measures moves with that font. `undefined` leaves the sheet default in force.
29
+ *
30
+ * Not bounded here, for the same reason {@link Row.height} is not, and for a stronger one:
31
+ * {@link MAX_COLUMN_WIDTH} bounds what Excel accepts being set, not what a file may hold. Excel
32
+ * reads a wider column back unchanged and re-saves it verbatim, so a bound here would refuse a
33
+ * width Excel itself preserves.
34
+ */
27
35
  get width(): number | undefined;
28
36
  set width(width: number | undefined);
29
37
  /** Whether the column is hidden. */
@@ -44,7 +44,15 @@ export class Column {
44
44
  set key(key) {
45
45
  this.#write('key', key);
46
46
  }
47
- /** Column width in character units; `undefined` leaves the sheet default in force. */
47
+ /**
48
+ * Column width in character units — digits of the workbook default font's maximum digit width,
49
+ * so what one unit measures moves with that font. `undefined` leaves the sheet default in force.
50
+ *
51
+ * Not bounded here, for the same reason {@link Row.height} is not, and for a stronger one:
52
+ * {@link MAX_COLUMN_WIDTH} bounds what Excel accepts being set, not what a file may hold. Excel
53
+ * reads a wider column back unchanged and re-saves it verbatim, so a bound here would refuse a
54
+ * width Excel itself preserves.
55
+ */
48
56
  get width() {
49
57
  return this.#read('width');
50
58
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The tallest row Excel accepts being set to, in points: it refuses 409.6 and takes 409.5. A row
3
+ * asked to hold more wrapped text than this cannot grow to fit it, and the overflow is simply not
4
+ * shown.
5
+ *
6
+ * A file may state more, and stating more loses the value rather than the file. Excel opens such a
7
+ * package without complaint and silently clamps the row — to 409.6, a tick *above* what it lets
8
+ * you assign, being 8192 twentieths of a point and so the width of the field it is read into — and
9
+ * writes 409.6 back on its next save. Check against this constant to keep a stated height from
10
+ * quietly becoming a different one.
11
+ */
12
+ export declare const MAX_ROW_HEIGHT = 409.5;
13
+ /**
14
+ * The widest column Excel accepts being set to, in character units of the workbook's default font.
15
+ * Excel refuses 255.4, so unlike the row-height ceiling this one is exactly integral.
16
+ *
17
+ * It is also the weaker of the two ceilings: it binds assignment only, and not a file at all.
18
+ * Excel honours a `width` of 1000 read from a package, renders the column at it, and round-trips
19
+ * it verbatim through its own save — where an over-limit row height is clamped away. So a width
20
+ * above this is a column no Excel user could have produced by dragging, not a value at risk.
21
+ *
22
+ * Character units, not points or pixels: a width is a count of digits of the default font's
23
+ * *maximum digit width*, which is why there is no companion `DEFAULT_COLUMN_WIDTH` constant here.
24
+ * The width a column takes when it states none is a function of that font — the familiar 8.43 holds
25
+ * for Calibri 11 and not for a workbook whose normal style says otherwise (Excel reports 8.09 for
26
+ * Aptos Narrow 11). `sheet.properties.defaultColWidth` is what a file declares, and
27
+ * docs/knowledge/specs/default-font-must-not-be-assumed-for-column-widths.md is why assuming a
28
+ * value for it is a bug rather than a shortcut.
29
+ */
30
+ export declare const MAX_COLUMN_WIDTH = 255;
@@ -0,0 +1,47 @@
1
+ // Excel's limits on the grid's *geometry* — the other half of the bounds `address.ts` states.
2
+ //
3
+ // `MAX_ROW`/`MAX_COLUMN` bound where a cell can be; these bound how big a line can be *set*. The
4
+ // difference that matters is who enforces them: the addressing bounds are structural (a reference
5
+ // past XFD is not a reference), so the model refuses them outright, while these are limits on
6
+ // *assignment* in Excel's own UI and object model. They are not limits on what a package may
7
+ // carry. The schema types `ht` and `width` as a bare `xsd:double` with no ceiling on either, and
8
+ // Excel opens an over-limit file clean — no repair prompt, no repair log — so a reader that threw
9
+ // on one would refuse a file Excel accepts. Nothing here is enforced, therefore, on either the
10
+ // read or the write path; these are for a caller that wants the size it states to be the size
11
+ // Excel uses, and the doc comments on `Row.height`/`Column.width` point here for that reason.
12
+ //
13
+ // What Excel does with an over-limit file is not symmetric, and the difference is why only one of
14
+ // these two numbers describes a value that survives: a row is silently clamped, a column is not.
15
+ // Both ceilings and both file behaviours were measured rather than quoted — Microsoft's own
16
+ // specifications page rounds the row-height ceiling to "409 points", and Excel takes 409.5. See
17
+ // docs/knowledge/specs/grid-geometry-limits-are-excels-not-the-schemas.md for the probes.
18
+ /**
19
+ * The tallest row Excel accepts being set to, in points: it refuses 409.6 and takes 409.5. A row
20
+ * asked to hold more wrapped text than this cannot grow to fit it, and the overflow is simply not
21
+ * shown.
22
+ *
23
+ * A file may state more, and stating more loses the value rather than the file. Excel opens such a
24
+ * package without complaint and silently clamps the row — to 409.6, a tick *above* what it lets
25
+ * you assign, being 8192 twentieths of a point and so the width of the field it is read into — and
26
+ * writes 409.6 back on its next save. Check against this constant to keep a stated height from
27
+ * quietly becoming a different one.
28
+ */
29
+ export const MAX_ROW_HEIGHT = 409.5;
30
+ /**
31
+ * The widest column Excel accepts being set to, in character units of the workbook's default font.
32
+ * Excel refuses 255.4, so unlike the row-height ceiling this one is exactly integral.
33
+ *
34
+ * It is also the weaker of the two ceilings: it binds assignment only, and not a file at all.
35
+ * Excel honours a `width` of 1000 read from a package, renders the column at it, and round-trips
36
+ * it verbatim through its own save — where an over-limit row height is clamped away. So a width
37
+ * above this is a column no Excel user could have produced by dragging, not a value at risk.
38
+ *
39
+ * Character units, not points or pixels: a width is a count of digits of the default font's
40
+ * *maximum digit width*, which is why there is no companion `DEFAULT_COLUMN_WIDTH` constant here.
41
+ * The width a column takes when it states none is a function of that font — the familiar 8.43 holds
42
+ * for Calibri 11 and not for a workbook whose normal style says otherwise (Excel reports 8.09 for
43
+ * Aptos Narrow 11). `sheet.properties.defaultColWidth` is what a file declares, and
44
+ * docs/knowledge/specs/default-font-must-not-be-assumed-for-column-widths.md is why assuming a
45
+ * value for it is a bug rather than a shortcut.
46
+ */
47
+ export const MAX_COLUMN_WIDTH = 255;
@@ -216,6 +216,10 @@ function textOf(item) {
216
216
  * Reduce any cell value to the scalar a pivot cache can hold: a number, a string, or a blank. Only
217
217
  * finite numbers stay numeric (a NaN would corrupt the cache); every other kind is flattened to its
218
218
  * displayed text so hostile or exotic source content can never throw or leak an object into the XML.
219
+ *
220
+ * Deliberately not `cellValueToText`, close as the two look: this classifies rather than renders
221
+ * (a number must reach the cache *as a number*), and its no-throw promise is the opposite of that
222
+ * function's, which rejects a value outside the union rather than quietly caching a blank.
219
223
  */
220
224
  function scalarOf(value) {
221
225
  if (value === null)
@@ -15,7 +15,15 @@ export declare class Row {
15
15
  * formatted, and they create the record on first write.
16
16
  */
17
17
  get properties(): Readonly<RowProperties> | undefined;
18
- /** Row height in points; `undefined` leaves the sheet default in force. */
18
+ /**
19
+ * Row height in points; `undefined` leaves the sheet default in force.
20
+ *
21
+ * Not bounded here, deliberately: {@link MAX_ROW_HEIGHT} is what Excel accepts *being set*, but
22
+ * the schema puts no ceiling on `ht` and this setter is also how the reader loads a foreign
23
+ * file, so refusing a taller row would mean refusing a file Excel opens clean. Check against the
24
+ * constant when authoring — Excel silently clamps a taller row on read, so a height above it is
25
+ * one you state and do not get.
26
+ */
19
27
  get height(): number | undefined;
20
28
  set height(height: number | undefined);
21
29
  /** Whether the row is hidden. */
package/dist/core/row.js CHANGED
@@ -35,7 +35,15 @@ export class Row {
35
35
  get properties() {
36
36
  return this.#sheet[INTERNAL].rowPropertiesOf(this.number);
37
37
  }
38
- /** Row height in points; `undefined` leaves the sheet default in force. */
38
+ /**
39
+ * Row height in points; `undefined` leaves the sheet default in force.
40
+ *
41
+ * Not bounded here, deliberately: {@link MAX_ROW_HEIGHT} is what Excel accepts *being set*, but
42
+ * the schema puts no ceiling on `ht` and this setter is also how the reader loads a foreign
43
+ * file, so refusing a taller row would mean refusing a file Excel opens clean. Check against the
44
+ * constant when authoring — Excel silently clamps a taller row on read, so a height above it is
45
+ * one you state and do not get.
46
+ */
39
47
  get height() {
40
48
  return this.#read('height');
41
49
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The number of lines `text` occupies when wrapped at `width` character units - the width unit a
3
+ * column states, so `estimateWrappedLines(cell, sheet.getColumn(2).width ?? 8.43)` is the shape of
4
+ * the call.
5
+ *
6
+ * A hard break opens a line of its own and what follows wraps independently, matching how Excel
7
+ * lays a wrapped cell out. The empty string is one line, not zero: a cell always occupies its row.
8
+ *
9
+ * An estimate, and only ever that. It counts characters, so it is exact for a monospaced face that
10
+ * wraps mid-word and approximate for every other - a run of `W`s wraps sooner on screen than this
11
+ * predicts, a run of `i`s later. Against Excel it reads a shade *low*, because Excel breaks at word
12
+ * boundaries and its usable width is about 0.64 character units under the stated one: measured at
13
+ * 5 lines where Excel laid out 6, 25 where Excel laid out 26. This exists so that a writer can
14
+ * state *a* height rather than leave one to the application that opens the file, and being within a
15
+ * line of the truth is what that needs.
16
+ *
17
+ * @throws {RangeError} if `width` is not a positive finite number - a column of zero width wraps
18
+ * nothing, and silently answering `Infinity` or `NaN` would put that straight into a row height.
19
+ */
20
+ export declare function estimateWrappedLines(text: string, width: number): number;
@@ -0,0 +1,50 @@
1
+ // How many lines a wrapped cell takes - the one measurement a writer needs and the format does
2
+ // not record.
3
+ //
4
+ // A row that states no height records no geometry, so the height is whoever opens the file's
5
+ // answer. Excel Desktop's answer is an auto-fit computed on open - promptly and correctly, but
6
+ // saturating at MAX_ROW_HEIGHT, so past ~28 lines it stops answering the question - and no answer
7
+ // at all is what a consumer that does not implement that auto-fit gets, this library's reader
8
+ // included. Stating a height is how the geometry stops depending on the reader, and to state one
9
+ // you have to know the line count. Measured in
10
+ // docs/knowledge/specs/rows-with-no-stated-height-are-autofitted-on-open.md, which also buries the
11
+ // claim this comment used to make, that such a sheet opens with blank unpainted bands. It does not.
12
+ //
13
+ // This counts *characters against a character-unit width*. It does not measure glyphs, and it is
14
+ // not the deferred font-metric question that
15
+ // docs/knowledge/specs/default-font-must-not-be-assumed-for-column-widths.md leaves open: a column
16
+ // width is already expressed in character units, so counting characters is dimensionally honest
17
+ // and needs no metric table. It is also, for the same reason, an approximation for any
18
+ // proportional face - `WWW` and `iii` are one character unit each here and are not on screen.
19
+ /**
20
+ * The number of lines `text` occupies when wrapped at `width` character units - the width unit a
21
+ * column states, so `estimateWrappedLines(cell, sheet.getColumn(2).width ?? 8.43)` is the shape of
22
+ * the call.
23
+ *
24
+ * A hard break opens a line of its own and what follows wraps independently, matching how Excel
25
+ * lays a wrapped cell out. The empty string is one line, not zero: a cell always occupies its row.
26
+ *
27
+ * An estimate, and only ever that. It counts characters, so it is exact for a monospaced face that
28
+ * wraps mid-word and approximate for every other - a run of `W`s wraps sooner on screen than this
29
+ * predicts, a run of `i`s later. Against Excel it reads a shade *low*, because Excel breaks at word
30
+ * boundaries and its usable width is about 0.64 character units under the stated one: measured at
31
+ * 5 lines where Excel laid out 6, 25 where Excel laid out 26. This exists so that a writer can
32
+ * state *a* height rather than leave one to the application that opens the file, and being within a
33
+ * line of the truth is what that needs.
34
+ *
35
+ * @throws {RangeError} if `width` is not a positive finite number - a column of zero width wraps
36
+ * nothing, and silently answering `Infinity` or `NaN` would put that straight into a row height.
37
+ */
38
+ export function estimateWrappedLines(text, width) {
39
+ if (!Number.isFinite(width) || width <= 0) {
40
+ throw new RangeError(`wrap width ${width} is not a positive finite number of character units`);
41
+ }
42
+ let lines = 0;
43
+ // A lone \r is not a break Excel writes, but a value assembled by a caller on Windows may carry
44
+ // one; counting it as a break costs nothing and mis-counting it as a character would show up as
45
+ // a row one line short.
46
+ for (const segment of text.split(/\r\n?|\n/)) {
47
+ lines += Math.max(1, Math.ceil(segment.length / width));
48
+ }
49
+ return lines;
50
+ }
@@ -81,11 +81,32 @@ export interface DataTableFormulaValue {
81
81
  }
82
82
  /** Everything a cell's value can be. `null` is the empty cell. */
83
83
  export type CellValue = null | number | string | boolean | Date | ErrorValue | FormulaValue | SharedFormulaValue | DataTableFormulaValue | RichTextValue | HyperlinkValue;
84
+ /**
85
+ * Whether a value is an in-cell error ({@link ErrorValue}). The narrowing counterpart of
86
+ * `detectValueType(value) === ValueType.Error`: use this one when the branch goes on to read
87
+ * `.error`, and {@link detectValueType} when it dispatches over all nine kinds at once.
88
+ */
84
89
  export declare function isErrorValue(value: CellValue): value is ErrorValue;
90
+ /**
91
+ * Whether a value is a cell's own formula ({@link FormulaValue}) — a master, or a formula
92
+ * belonging to no shared group. A shared-formula clone is **not** one of these; see
93
+ * {@link isSharedFormulaValue}. Both report as `ValueType.Formula`, so a caller that means "any
94
+ * formula-shaped cell" wants {@link detectValueType}, not this.
95
+ */
85
96
  export declare function isFormulaValue(value: CellValue): value is FormulaValue;
97
+ /** Whether a value is a clone participating in a shared formula ({@link SharedFormulaValue}). */
86
98
  export declare function isSharedFormulaValue(value: CellValue): value is SharedFormulaValue;
99
+ /** Whether a value is a What-If-Analysis data-table formula ({@link DataTableFormulaValue}). */
87
100
  export declare function isDataTableFormulaValue(value: CellValue): value is DataTableFormulaValue;
101
+ /**
102
+ * Whether a value is composed of formatted runs ({@link RichTextValue}). This is the test to
103
+ * make before {@link richTextToPlain}, which accepts nothing else.
104
+ */
88
105
  export declare function isRichTextValue(value: CellValue): value is RichTextValue;
106
+ /**
107
+ * Whether a value is a hyperlink ({@link HyperlinkValue}). Note that its `text` is itself either
108
+ * a string or a {@link RichTextValue}, so reading the label out means one more narrowing.
109
+ */
89
110
  export declare function isHyperlinkValue(value: CellValue): value is HyperlinkValue;
90
111
  /**
91
112
  * Flatten a rich-text value to its plain text by concatenating every run's text in order. This is the
@@ -93,6 +114,25 @@ export declare function isHyperlinkValue(value: CellValue): value is HyperlinkVa
93
114
  * the string a rich cell reads as when its formatting is discarded.
94
115
  */
95
116
  export declare function richTextToPlain(value: RichTextValue): string;
117
+ /**
118
+ * The plain text of any cell value — total over {@link CellValue}, so a caller reading a sheet
119
+ * whose cells it did not write never has to switch on the union itself.
120
+ *
121
+ * This is the *value's* text, not the cell's *display* text: a number renders as JavaScript
122
+ * renders it, with no number format applied (`0.1 + 0.2` is `"0.30000000000000004"`, a currency
123
+ * cell has no currency sign), because the format lives on the style and this function is given
124
+ * only the value. What each kind yields:
125
+ *
126
+ * - the empty cell (`null`) and an invalid `Date` → `""`, the two ways a cell has no text
127
+ * - a boolean → `"TRUE"` / `"FALSE"`, Excel's own literals rather than JavaScript's
128
+ * - a `Date` → a full ISO-8601 timestamp
129
+ * - an error → its literal, e.g. `"#REF!"` — the same string the grid shows
130
+ * - rich text → every run concatenated ({@link richTextToPlain})
131
+ * - a hyperlink → its label, never its destination
132
+ * - any of the three formula kinds → the text of the *cached result*, and `""` when the cell
133
+ * carries no cached result: the formula source is not text the sheet ever displayed
134
+ */
135
+ export declare function cellValueToText(value: CellValue): string;
96
136
  /**
97
137
  * Classify a value into its observable {@link ValueType}. This is total over
98
138
  * {@link CellValue}: every legal value has exactly one type. A `Date` is a date even
@@ -38,23 +38,44 @@ const ERROR_SET = new Set(ERROR_CODES);
38
38
  function hasKey(value, key) {
39
39
  return typeof value === 'object' && value !== null && key in value;
40
40
  }
41
+ /**
42
+ * Whether a value is an in-cell error ({@link ErrorValue}). The narrowing counterpart of
43
+ * `detectValueType(value) === ValueType.Error`: use this one when the branch goes on to read
44
+ * `.error`, and {@link detectValueType} when it dispatches over all nine kinds at once.
45
+ */
41
46
  export function isErrorValue(value) {
42
47
  return hasKey(value, 'error');
43
48
  }
49
+ /**
50
+ * Whether a value is a cell's own formula ({@link FormulaValue}) — a master, or a formula
51
+ * belonging to no shared group. A shared-formula clone is **not** one of these; see
52
+ * {@link isSharedFormulaValue}. Both report as `ValueType.Formula`, so a caller that means "any
53
+ * formula-shaped cell" wants {@link detectValueType}, not this.
54
+ */
44
55
  export function isFormulaValue(value) {
45
56
  // A shared-formula clone resolved on read carries both its master address (`sharedFormula`) and the
46
57
  // translated `formula`; it is a SharedFormulaValue, so exclude it here to keep the two kinds distinct.
47
58
  return hasKey(value, 'formula') && !('sharedFormula' in value);
48
59
  }
60
+ /** Whether a value is a clone participating in a shared formula ({@link SharedFormulaValue}). */
49
61
  export function isSharedFormulaValue(value) {
50
62
  return hasKey(value, 'sharedFormula');
51
63
  }
64
+ /** Whether a value is a What-If-Analysis data-table formula ({@link DataTableFormulaValue}). */
52
65
  export function isDataTableFormulaValue(value) {
53
66
  return hasKey(value, 'shareType') && value.shareType === 'dataTable';
54
67
  }
68
+ /**
69
+ * Whether a value is composed of formatted runs ({@link RichTextValue}). This is the test to
70
+ * make before {@link richTextToPlain}, which accepts nothing else.
71
+ */
55
72
  export function isRichTextValue(value) {
56
73
  return hasKey(value, 'richText');
57
74
  }
75
+ /**
76
+ * Whether a value is a hyperlink ({@link HyperlinkValue}). Note that its `text` is itself either
77
+ * a string or a {@link RichTextValue}, so reading the label out means one more narrowing.
78
+ */
58
79
  export function isHyperlinkValue(value) {
59
80
  return hasKey(value, 'hyperlink');
60
81
  }
@@ -66,6 +87,54 @@ export function isHyperlinkValue(value) {
66
87
  export function richTextToPlain(value) {
67
88
  return value.richText.map((run) => run.text).join('');
68
89
  }
90
+ /**
91
+ * The plain text of any cell value — total over {@link CellValue}, so a caller reading a sheet
92
+ * whose cells it did not write never has to switch on the union itself.
93
+ *
94
+ * This is the *value's* text, not the cell's *display* text: a number renders as JavaScript
95
+ * renders it, with no number format applied (`0.1 + 0.2` is `"0.30000000000000004"`, a currency
96
+ * cell has no currency sign), because the format lives on the style and this function is given
97
+ * only the value. What each kind yields:
98
+ *
99
+ * - the empty cell (`null`) and an invalid `Date` → `""`, the two ways a cell has no text
100
+ * - a boolean → `"TRUE"` / `"FALSE"`, Excel's own literals rather than JavaScript's
101
+ * - a `Date` → a full ISO-8601 timestamp
102
+ * - an error → its literal, e.g. `"#REF!"` — the same string the grid shows
103
+ * - rich text → every run concatenated ({@link richTextToPlain})
104
+ * - a hyperlink → its label, never its destination
105
+ * - any of the three formula kinds → the text of the *cached result*, and `""` when the cell
106
+ * carries no cached result: the formula source is not text the sheet ever displayed
107
+ */
108
+ export function cellValueToText(value) {
109
+ if (value === null)
110
+ return '';
111
+ switch (typeof value) {
112
+ case 'number':
113
+ return String(value);
114
+ case 'string':
115
+ return value;
116
+ case 'boolean':
117
+ return value ? 'TRUE' : 'FALSE';
118
+ default:
119
+ break;
120
+ }
121
+ if (value instanceof Date)
122
+ return Number.isNaN(value.getTime()) ? '' : value.toISOString();
123
+ // Same precedence as detectValueType, and for the same reason: the outer shape wins, so a
124
+ // hyperlink whose label is rich text renders as a hyperlink's label, not as rich text.
125
+ if (isHyperlinkValue(value)) {
126
+ return typeof value.text === 'string' ? value.text : richTextToPlain(value.text);
127
+ }
128
+ // Every formula kind carries a `result` of the same optional shape; one recursion renders it.
129
+ if (isFormulaValue(value) || isSharedFormulaValue(value) || isDataTableFormulaValue(value)) {
130
+ return value.result === undefined ? '' : cellValueToText(value.result);
131
+ }
132
+ if (isRichTextValue(value))
133
+ return richTextToPlain(value);
134
+ if (isErrorValue(value))
135
+ return value.error;
136
+ return unsupportedValue(value);
137
+ }
69
138
  /**
70
139
  * Classify a value into its observable {@link ValueType}. This is total over
71
140
  * {@link CellValue}: every legal value has exactly one type. A `Date` is a date even
@@ -98,6 +167,13 @@ export function detectValueType(value) {
98
167
  return ValueType.RichText;
99
168
  if (isErrorValue(value))
100
169
  return ValueType.Error;
170
+ return unsupportedValue(value);
171
+ }
172
+ /**
173
+ * The verdict "this is not a cell value at all", raised from one place so that every function
174
+ * total over {@link CellValue} rejects the same inputs with the same message.
175
+ */
176
+ function unsupportedValue(value) {
101
177
  throw new TypeError(`unsupported cell value: ${describe(value)}`);
102
178
  }
103
179
  /** Whether a string is one of Excel's canonical error literals. */
@@ -444,6 +444,19 @@ export declare class Workbook {
444
444
  addWorksheet(name: string, options?: AddWorksheetOptions): Worksheet;
445
445
  /** Look up a worksheet by name (case-insensitive) or by numeric id. */
446
446
  getWorksheet(nameOrId: string | number): Worksheet | undefined;
447
+ /**
448
+ * {@link getWorksheet}, for a caller who knows the sheet is there — the miss throws instead of
449
+ * returning `undefined`, and the message names every sheet the workbook does have.
450
+ *
451
+ * The partial lookup is the right primitive for asking *whether* a sheet exists, and the wrong
452
+ * one for reaching a sheet a template is expected to carry: `undefined` flows on into a `?.`
453
+ * chain and fails several steps later with nothing left to say about which name was missing.
454
+ * That listing is the whole point — a lookup miss is a typo, a stale template or a renamed tab,
455
+ * and all three are answered by seeing the real names.
456
+ *
457
+ * @throws {AuthoringError} if no worksheet has that name (case-insensitive) or numeric id.
458
+ */
459
+ requireWorksheet(nameOrId: string | number): Worksheet;
447
460
  /**
448
461
  * The codec's channel into this workbook — see `core/internal.ts` for why these are not public
449
462
  * methods. Declared last so every private field it closes over is already in scope.
@@ -673,6 +673,29 @@ export class Workbook {
673
673
  const target = nameOrId.toLowerCase();
674
674
  return this.#worksheets.find((sheet) => sheet.name.toLowerCase() === target);
675
675
  }
676
+ /**
677
+ * {@link getWorksheet}, for a caller who knows the sheet is there — the miss throws instead of
678
+ * returning `undefined`, and the message names every sheet the workbook does have.
679
+ *
680
+ * The partial lookup is the right primitive for asking *whether* a sheet exists, and the wrong
681
+ * one for reaching a sheet a template is expected to carry: `undefined` flows on into a `?.`
682
+ * chain and fails several steps later with nothing left to say about which name was missing.
683
+ * That listing is the whole point — a lookup miss is a typo, a stale template or a renamed tab,
684
+ * and all three are answered by seeing the real names.
685
+ *
686
+ * @throws {AuthoringError} if no worksheet has that name (case-insensitive) or numeric id.
687
+ */
688
+ requireWorksheet(nameOrId) {
689
+ const sheet = this.getWorksheet(nameOrId);
690
+ if (sheet !== undefined)
691
+ return sheet;
692
+ const wanted = typeof nameOrId === 'number' ? `id ${nameOrId}` : JSON.stringify(nameOrId);
693
+ if (this.#worksheets.length === 0) {
694
+ throw new AuthoringError(`no worksheet ${wanted}: this workbook has no worksheets`);
695
+ }
696
+ const have = this.#worksheets.map((sheet) => JSON.stringify(sheet.name)).join(', ');
697
+ throw new AuthoringError(`no worksheet ${wanted}; this workbook has ${have}`);
698
+ }
676
699
  #assertValidSheetName(name) {
677
700
  if (name.length === 0) {
678
701
  throw new AuthoringError('worksheet name cannot be empty');
@@ -259,6 +259,28 @@ export declare class Worksheet {
259
259
  * other axis: a value in column E makes this 5 even if columns B–D are empty.
260
260
  */
261
261
  get columnCount(): number;
262
+ /**
263
+ * The sheet's used range as one handle — `A1` through the last row and column that carry
264
+ * anything — or `undefined` when there is no rectangle to name.
265
+ *
266
+ * This is {@link rowCount} and {@link columnCount} said once, so a caller stops reassembling
267
+ * `A1:${numberToColumn(sheet.columnCount)}${sheet.rowCount}` by hand. That is what an
268
+ * {@link autoFilter} covering the whole sheet wants — `sheet.autoFilter = sheet.usedRange.address`
269
+ * — and Excel writes exactly that ref for a filter it applies itself. A header-only ref filters
270
+ * nothing, which is the bug this exists to make hard to write.
271
+ *
272
+ * It inherits both counts' definition of *used*, so it spans gaps (a value in `E5` and nothing
273
+ * else still gives `A1:E5`) and includes a line carrying only its own formatting — a set column
274
+ * width, an outline level, a merge reaching past the last value. `undefined` therefore means
275
+ * strictly "no rectangle": an empty sheet, or one carrying only row formatting and no columns at
276
+ * all (or the reverse), where an axis has no extent to bound the other against.
277
+ *
278
+ * Not the same thing as the `<dimension>` a written package records. That is the *tight* box —
279
+ * top-left at the first used cell, formatting-only rows excluded — because Excel writes it to
280
+ * describe where the data is, not what the grid spans. This handle is anchored at `A1`, because
281
+ * a caller asking for the used range means the block to read, style or filter.
282
+ */
283
+ get usedRange(): Range | undefined;
262
284
  /** The columns carrying format properties, as handles, in ascending index order. */
263
285
  columns(): IterableIterator<Column>;
264
286
  /**
@@ -260,6 +260,34 @@ export class Worksheet {
260
260
  }
261
261
  return last;
262
262
  }
263
+ /**
264
+ * The sheet's used range as one handle — `A1` through the last row and column that carry
265
+ * anything — or `undefined` when there is no rectangle to name.
266
+ *
267
+ * This is {@link rowCount} and {@link columnCount} said once, so a caller stops reassembling
268
+ * `A1:${numberToColumn(sheet.columnCount)}${sheet.rowCount}` by hand. That is what an
269
+ * {@link autoFilter} covering the whole sheet wants — `sheet.autoFilter = sheet.usedRange.address`
270
+ * — and Excel writes exactly that ref for a filter it applies itself. A header-only ref filters
271
+ * nothing, which is the bug this exists to make hard to write.
272
+ *
273
+ * It inherits both counts' definition of *used*, so it spans gaps (a value in `E5` and nothing
274
+ * else still gives `A1:E5`) and includes a line carrying only its own formatting — a set column
275
+ * width, an outline level, a merge reaching past the last value. `undefined` therefore means
276
+ * strictly "no rectangle": an empty sheet, or one carrying only row formatting and no columns at
277
+ * all (or the reverse), where an axis has no extent to bound the other against.
278
+ *
279
+ * Not the same thing as the `<dimension>` a written package records. That is the *tight* box —
280
+ * top-left at the first used cell, formatting-only rows excluded — because Excel writes it to
281
+ * describe where the data is, not what the grid spans. This handle is anchored at `A1`, because
282
+ * a caller asking for the used range means the block to read, style or filter.
283
+ */
284
+ get usedRange() {
285
+ const bottom = this.rowCount;
286
+ const right = this.columnCount;
287
+ if (bottom === 0 || right === 0)
288
+ return undefined;
289
+ return new Range(this, 1, 1, bottom, right);
290
+ }
263
291
  /** The columns carrying format properties, as handles, in ascending index order. */
264
292
  *columns() {
265
293
  for (const index of [...this.#columns.keys()].sort((a, b) => a - b)) {
@@ -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';
@@ -9,12 +9,14 @@ export { Cell } from '../core/cell.js';
9
9
  export { applyTint, DEFAULT_INDEXED_COLORS, resolveColor, SYSTEM_INDEXED_COLORS, } from '../core/color-resolution.js';
10
10
  export { Column } from '../core/column.js';
11
11
  export { isOneCellAnchor, PX_TO_EMU, } from '../core/image.js';
12
+ export { MAX_COLUMN_WIDTH, MAX_ROW_HEIGHT } from '../core/limits.js';
12
13
  export { PivotTable, } from '../core/pivot-table.js';
13
14
  export { Range } from '../core/range.js';
14
15
  export { Row } from '../core/row.js';
15
16
  export { Table, } from '../core/table.js';
16
17
  export { isTableStyleElementType, STRIPE_ELEMENT_TYPES, TABLE_STYLE_ELEMENT_TYPES, } from '../core/table-style.js';
18
+ export { estimateWrappedLines } from '../core/text-metrics.js';
17
19
  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';
20
+ export { cellValueToText, coerceCellValue, detectValueType, ERROR_CODES, isDataTableFormulaValue, isErrorCode, isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, richTextToPlain, ValueType, } from '../core/value.js';
19
21
  export { DEFAULT_WORKBOOK_VIEW, Workbook, } from '../core/workbook.js';
20
22
  export { Worksheet, } from '../core/worksheet.js';
@@ -4,12 +4,14 @@
4
4
  // text. The lossy direction (styles, formulas-as-formulas, multiple sheets) is inherent to the
5
5
  // format, so this writer makes the honest choices explicit: one selected sheet, each row sized to
6
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.
7
+ // cached result, a Date rendered by a caller-supplied format or a full ISO-8601 timestamp. What a
8
+ // value reads as is `cellValueToText`'s answer, not a private one — a CSV field and `cell.text`
9
+ // disagreeing about the same cell would be a bug in one of them.
8
10
  //
9
11
  // `writeCsvText` yields the logical text; `writeCsv` encodes it to bytes and — for UTF-8, the
10
12
  // default — prepends a byte-order mark so a consumer such as Excel detects the encoding and does
11
13
  // 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';
14
+ import { cellValueToText, isDataTableFormulaValue, isFormulaValue, isSharedFormulaValue, } from '../../core/value.js';
13
15
  import { AuthoringError } from '../../errors.js';
14
16
  const UTF8_BOM = Uint8Array.of(0xef, 0xbb, 0xbf);
15
17
  /** The logical CSV text of one worksheet — no BOM, no byte encoding. */
@@ -56,33 +58,19 @@ function selectSheet(workbook, name) {
56
58
  throw new AuthoringError('workbook has no worksheet to write as CSV');
57
59
  return first;
58
60
  }
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;
61
+ return workbook.requireWorksheet(name);
63
62
  }
63
+ // A field is the value's plain text, with one CSV-only deviation: `dateFormat`/`dateUTC` let a
64
+ // caller render dates in something other than ISO-8601. That reaches inside a formula's cached
65
+ // result too, which is why the recursion is here rather than delegated wholesale.
64
66
  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)
67
+ if (value instanceof Date && options.dateFormat !== undefined) {
74
68
  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
69
  }
82
- if (isFormulaValue(value) || isSharedFormulaValue(value)) {
70
+ if (isFormulaValue(value) || isSharedFormulaValue(value) || isDataTableFormulaValue(value)) {
83
71
  return value.result === undefined ? '' : csvFieldText(value.result, options);
84
72
  }
85
- return '';
73
+ return cellValueToText(value);
86
74
  }
87
75
  const DATE_TOKENS = /YYYY|YY|MM|DD|HH|mm|ss|M|D|H|m|s/g;
88
76
  function formatDate(date, format, utc) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shbernal/ts-xlsx",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "A TypeScript-first library for reading and writing xlsx (OOXML) spreadsheets.",
5
5
  "license": "MIT",
6
6
  "author": "shbernal",
@@ -64,7 +64,7 @@
64
64
  "README.md"
65
65
  ],
66
66
  "scripts": {
67
- "prepare": "lefthook install",
67
+ "prepare": "node scripts/install-hooks.ts",
68
68
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
69
69
  "build": "pnpm run clean && tsc -p tsconfig.build.json",
70
70
  "size": "node scripts/size-budget.ts",