@shbernal/ts-xlsx 3.0.0 → 3.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.
- package/README.md +34 -24
- package/dist/bytes.d.ts +10 -2
- package/dist/bytes.js +22 -0
- package/dist/core/address.d.ts +96 -2
- package/dist/core/address.js +87 -33
- package/dist/core/array-formula-ranges.d.ts +33 -0
- package/dist/core/array-formula-ranges.js +62 -0
- package/dist/core/autofilter.d.ts +4 -3
- package/dist/core/autofilter.js +31 -30
- package/dist/core/axis-handle.d.ts +2 -0
- package/dist/core/axis-handle.js +5 -2
- package/dist/core/cell.d.ts +4 -3
- package/dist/core/cell.js +4 -6
- package/dist/core/clone.d.ts +29 -0
- package/dist/core/clone.js +24 -0
- package/dist/core/color-resolution.js +4 -6
- package/dist/core/column.d.ts +1 -0
- package/dist/core/column.js +6 -2
- package/dist/core/comment-thread.d.ts +7 -3
- package/dist/core/comment-thread.js +2 -1
- package/dist/core/conditional-formatting-overlay.d.ts +11 -1
- package/dist/core/conditional-formatting-overlay.js +30 -3
- package/dist/core/conditional-formatting.d.ts +107 -11
- package/dist/core/conditional-formatting.js +76 -19
- package/dist/core/containers.d.ts +12 -0
- package/dist/core/containers.js +5 -0
- package/dist/core/data-validation-overlay.d.ts +7 -1
- package/dist/core/data-validation-overlay.js +19 -4
- package/dist/core/data-validation.d.ts +22 -0
- package/dist/core/data-validation.js +30 -4
- package/dist/core/date-format.d.ts +20 -0
- package/dist/core/date-format.js +170 -0
- package/dist/core/date.d.ts +43 -8
- package/dist/core/date.js +19 -3
- package/dist/core/formula-references.d.ts +26 -0
- package/dist/core/formula-references.js +173 -0
- package/dist/core/formula-scan.d.ts +2 -0
- package/dist/core/formula-scan.js +58 -0
- package/dist/core/formula.d.ts +76 -17
- package/dist/core/formula.js +87 -80
- package/dist/core/function-values.d.ts +4 -0
- package/dist/core/function-values.js +58 -0
- package/dist/core/future-functions.d.ts +3 -0
- package/dist/core/{modern-functions.js → future-functions.js} +119 -88
- package/dist/core/grid-edits.d.ts +30 -3
- package/dist/core/grid-edits.js +248 -81
- package/dist/core/grid-shift.d.ts +58 -9
- package/dist/core/grid-shift.js +26 -4
- package/dist/core/hyperlink.d.ts +39 -0
- package/dist/core/hyperlink.js +88 -0
- package/dist/core/image.d.ts +49 -14
- package/dist/core/image.js +16 -5
- package/dist/core/internal.d.ts +10 -2
- package/dist/core/limits.d.ts +25 -0
- package/dist/core/limits.js +4 -0
- package/dist/core/merge.d.ts +18 -4
- package/dist/core/merge.js +33 -20
- package/dist/core/page-setup.d.ts +26 -11
- package/dist/core/pivot-table.d.ts +49 -10
- package/dist/core/pivot-table.js +112 -60
- package/dist/core/protection.d.ts +66 -4
- package/dist/core/protection.js +6 -11
- package/dist/core/range.js +34 -30
- package/dist/core/row-input.js +2 -2
- package/dist/core/row.d.ts +1 -0
- package/dist/core/row.js +6 -2
- package/dist/core/structured-reference.d.ts +7 -0
- package/dist/core/structured-reference.js +7 -0
- package/dist/core/style.d.ts +85 -70
- package/dist/core/style.js +92 -28
- package/dist/core/table-style.d.ts +1 -1
- package/dist/core/table-style.js +4 -4
- package/dist/core/table.d.ts +64 -16
- package/dist/core/table.js +87 -40
- package/dist/core/theme.d.ts +2 -0
- package/dist/core/theme.js +9 -5
- package/dist/core/value.d.ts +60 -28
- package/dist/core/value.js +48 -43
- package/dist/core/workbook-media.d.ts +34 -0
- package/dist/core/workbook-media.js +44 -0
- package/dist/core/workbook-styles.d.ts +83 -1
- package/dist/core/workbook-styles.js +4 -0
- package/dist/core/workbook-theme.js +5 -3
- package/dist/core/workbook-vba.js +3 -2
- package/dist/core/workbook.d.ts +41 -3
- package/dist/core/workbook.js +46 -41
- package/dist/core/worksheet-comments.d.ts +2 -1
- package/dist/core/worksheet-comments.js +20 -13
- package/dist/core/worksheet-merges.d.ts +49 -0
- package/dist/core/worksheet-merges.js +65 -0
- package/dist/core/worksheet-model.d.ts +10 -2
- package/dist/core/worksheet-model.js +28 -9
- package/dist/core/worksheet-pictures.d.ts +3 -3
- package/dist/core/worksheet-pictures.js +7 -6
- package/dist/core/worksheet.d.ts +165 -27
- package/dist/core/worksheet.js +206 -89
- package/dist/customui/ribbon.js +2 -1
- package/dist/entries/core.d.ts +21 -14
- package/dist/entries/core.js +14 -7
- package/dist/entries/errors.d.ts +1 -0
- package/dist/entries/errors.js +1 -0
- package/dist/entries/xlsx.d.ts +1 -1
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +22 -0
- package/dist/hex.d.ts +8 -0
- package/dist/hex.js +3 -0
- package/dist/io/cell-metadata/metadata.d.ts +42 -0
- package/dist/io/cell-metadata/metadata.js +35 -0
- package/dist/io/cell-metadata/rich-values.d.ts +11 -0
- package/dist/io/cell-metadata/rich-values.js +80 -0
- package/dist/io/csv/delimiter.d.ts +21 -0
- package/dist/io/csv/delimiter.js +18 -0
- package/dist/io/csv/errors.d.ts +14 -0
- package/dist/io/csv/errors.js +5 -0
- package/dist/io/csv/read.d.ts +6 -1
- package/dist/io/csv/read.js +51 -22
- package/dist/io/csv/write.d.ts +19 -5
- package/dist/io/csv/write.js +24 -44
- package/dist/io/opc/inflate.js +14 -0
- package/dist/io/opc/namespaces.d.ts +16 -0
- package/dist/io/opc/namespaces.js +4 -0
- package/dist/io/opc/part-paths.d.ts +9 -0
- package/dist/io/opc/part-paths.js +5 -5
- package/dist/io/opc/read-opc.d.ts +58 -14
- package/dist/io/opc/read-opc.js +49 -21
- package/dist/io/opc/read-options.d.ts +5 -1
- package/dist/io/opc/sniff-format.d.ts +3 -3
- package/dist/io/opc/sniff-format.js +2 -2
- package/dist/io/opc/strict-relationships.d.ts +2 -0
- package/dist/io/opc/strict-relationships.js +12 -0
- package/dist/io/read-policy/column-budget.d.ts +26 -0
- package/dist/io/read-policy/column-budget.js +18 -0
- package/dist/io/read-policy/read-repair.d.ts +74 -0
- package/dist/io/read-policy/read-repair.js +119 -0
- package/dist/io/style/cell-style-resolution.d.ts +25 -0
- package/dist/io/style/cell-style-resolution.js +30 -0
- package/dist/io/style/xf-style.d.ts +23 -1
- package/dist/io/style/xf-style.js +8 -0
- package/dist/io/xlsb/formula.d.ts +2 -2
- package/dist/io/xlsb/formula.js +52 -35
- package/dist/io/xlsb/primitives.js +8 -14
- package/dist/io/xlsb/ptg-functions.d.ts +19 -10
- package/dist/io/xlsb/ptg-functions.js +495 -596
- package/dist/io/xlsb/read-metadata.d.ts +7 -0
- package/dist/io/xlsb/read-metadata.js +69 -0
- package/dist/io/xlsb/read-styles.js +45 -81
- package/dist/io/xlsb/read-worksheet.d.ts +26 -1
- package/dist/io/xlsb/read-worksheet.js +148 -106
- package/dist/io/xlsb/read.d.ts +6 -3
- package/dist/io/xlsb/read.js +70 -31
- package/dist/io/xlsb/record-stream.d.ts +30 -0
- package/dist/io/xlsb/record-stream.js +20 -0
- package/dist/io/xlsb/record-types.d.ts +14 -5
- package/dist/io/xlsb/record-types.js +14 -5
- package/dist/io/xlsb/sheet-protection.d.ts +20 -0
- package/dist/io/xlsb/sheet-protection.js +60 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +24 -9
- package/dist/io/xlsx/cell-accumulator.js +74 -21
- package/dist/io/xlsx/cell-metadata.d.ts +39 -0
- package/dist/io/xlsx/cell-metadata.js +162 -0
- package/dist/io/xlsx/cell-value.d.ts +19 -5
- package/dist/io/xlsx/cell-value.js +45 -30
- package/dist/io/xlsx/color-xml.js +3 -2
- package/dist/io/xlsx/column-span.d.ts +11 -0
- package/dist/io/xlsx/column-span.js +9 -0
- package/dist/io/xlsx/comments.d.ts +9 -30
- package/dist/io/xlsx/comments.js +25 -83
- package/dist/io/xlsx/conditional-formatting.d.ts +43 -23
- package/dist/io/xlsx/conditional-formatting.js +437 -138
- package/dist/io/xlsx/data-validation.d.ts +12 -7
- package/dist/io/xlsx/data-validation.js +67 -53
- package/dist/io/xlsx/edit-vba.d.ts +7 -2
- package/dist/io/xlsx/edit-vba.js +52 -52
- package/dist/io/xlsx/font-xml.d.ts +7 -0
- package/dist/io/xlsx/font-xml.js +104 -0
- package/dist/io/xlsx/hyperlinks.d.ts +19 -21
- package/dist/io/xlsx/hyperlinks.js +12 -32
- package/dist/io/xlsx/images.d.ts +49 -16
- package/dist/io/xlsx/images.js +149 -50
- package/dist/io/xlsx/namespaces.d.ts +24 -0
- package/dist/io/xlsx/namespaces.js +11 -0
- package/dist/io/xlsx/package-plan.d.ts +60 -15
- package/dist/io/xlsx/package-plan.js +83 -18
- package/dist/io/xlsx/part-names.d.ts +6 -8
- package/dist/io/xlsx/part-names.js +3 -7
- package/dist/io/xlsx/pivot.d.ts +5 -5
- package/dist/io/xlsx/pivot.js +26 -24
- package/dist/io/xlsx/preserved-splices.d.ts +6 -0
- package/dist/io/xlsx/preserved-splices.js +74 -0
- package/dist/io/xlsx/read-comments.d.ts +29 -0
- package/dist/io/xlsx/read-comments.js +75 -0
- package/dist/io/xlsx/read-parts.d.ts +59 -0
- package/dist/io/xlsx/read-parts.js +259 -0
- package/dist/io/xlsx/read-pivot.js +11 -4
- package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +8 -5
- package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +24 -4
- package/dist/io/xlsx/read-rows.d.ts +35 -11
- package/dist/io/xlsx/read-rows.js +73 -49
- package/dist/io/xlsx/read-shared-strings.js +3 -2
- package/dist/io/xlsx/read-styles.d.ts +0 -30
- package/dist/io/xlsx/read-styles.js +45 -105
- package/dist/io/xlsx/read-workbook-xml.d.ts +42 -0
- package/dist/io/xlsx/read-workbook-xml.js +175 -0
- package/dist/io/xlsx/read-worksheet.d.ts +9 -3
- package/dist/io/xlsx/read-worksheet.js +70 -177
- package/dist/io/xlsx/read.d.ts +3 -14
- package/dist/io/xlsx/read.js +74 -411
- package/dist/io/xlsx/relationships.d.ts +3 -0
- package/dist/io/xlsx/relationships.js +4 -0
- package/dist/io/xlsx/rich-text.js +1 -1
- package/dist/io/xlsx/row-position.d.ts +20 -0
- package/dist/io/xlsx/row-position.js +11 -0
- package/dist/io/xlsx/row-xml.d.ts +139 -0
- package/dist/io/xlsx/row-xml.js +222 -0
- package/dist/io/xlsx/shared-formulas.js +2 -2
- package/dist/io/xlsx/sheet-properties.d.ts +10 -1
- package/dist/io/xlsx/sheet-properties.js +182 -20
- package/dist/io/xlsx/strict-parts.d.ts +7 -0
- package/dist/io/xlsx/strict-parts.js +171 -0
- package/dist/io/xlsx/style-elements.d.ts +41 -0
- package/dist/io/xlsx/style-elements.js +136 -0
- package/dist/io/xlsx/styles.d.ts +17 -3
- package/dist/io/xlsx/styles.js +13 -209
- package/dist/io/xlsx/tables.d.ts +10 -2
- package/dist/io/xlsx/tables.js +55 -52
- package/dist/io/xlsx/theme-xml.d.ts +32 -4
- package/dist/io/xlsx/theme-xml.js +106 -71
- package/dist/io/xlsx/threaded-comments.d.ts +14 -11
- package/dist/io/xlsx/threaded-comments.js +16 -8
- package/dist/io/xlsx/workbook-xml.d.ts +45 -6
- package/dist/io/xlsx/workbook-xml.js +62 -54
- package/dist/io/xlsx/worksheet-xml.d.ts +26 -70
- package/dist/io/xlsx/worksheet-xml.js +69 -282
- package/dist/io/xlsx/write-stream.d.ts +90 -19
- package/dist/io/xlsx/write-stream.js +80 -25
- package/dist/io/xlsx/write.d.ts +12 -6
- package/dist/io/xlsx/write.js +226 -123
- package/dist/rel-type.d.ts +25 -0
- package/dist/rel-type.js +9 -0
- package/dist/token-set.d.ts +13 -0
- package/dist/token-set.js +4 -1
- package/dist/vba/bytes.d.ts +16 -5
- package/dist/vba/bytes.js +27 -6
- package/dist/vba/cfb-format.d.ts +55 -0
- package/dist/vba/cfb-format.js +35 -0
- package/dist/vba/cfb-writer.d.ts +8 -0
- package/dist/vba/cfb-writer.js +40 -40
- package/dist/vba/cfb.d.ts +21 -4
- package/dist/vba/cfb.js +124 -90
- package/dist/vba/codepage.js +2 -1
- package/dist/vba/dir-records.d.ts +6 -0
- package/dist/vba/dir-records.js +10 -2
- package/dist/vba/errors.d.ts +11 -4
- package/dist/vba/index.d.ts +0 -2
- package/dist/vba/index.js +0 -2
- package/dist/vba/ms-ovba.d.ts +6 -2
- package/dist/vba/ms-ovba.js +66 -22
- package/dist/vba/project-editor.js +129 -119
- package/dist/vba/project.d.ts +15 -1
- package/dist/vba/project.js +38 -13
- package/dist/vba/vba-encoding.d.ts +4 -10
- package/dist/vba/vba-encoding.js +3 -17
- package/dist/xml/xml-attrs.d.ts +37 -0
- package/dist/xml/xml-attrs.js +30 -0
- package/dist/xml/xml-chars.d.ts +40 -0
- package/dist/xml/xml-chars.js +12 -0
- package/dist/xml/xml-namespaces.d.ts +31 -0
- package/dist/xml/xml-namespaces.js +50 -0
- package/dist/xml/xml-read.d.ts +101 -11
- package/dist/xml/xml-read.js +150 -18
- package/dist/xml/xml-scan.d.ts +25 -34
- package/dist/xml/xml-scan.js +69 -46
- package/dist/xml/xml.d.ts +59 -26
- package/dist/xml/xml.js +45 -23
- package/package.json +15 -9
- package/skills/ts-xlsx-upstream/SKILL.md +21 -13
- package/dist/core/modern-functions.d.ts +0 -1
package/dist/errors.d.ts
CHANGED
|
@@ -77,3 +77,67 @@ export declare class InternalError extends XlsxError {
|
|
|
77
77
|
readonly code = "internal";
|
|
78
78
|
constructor(message?: string, options?: ErrorOptions);
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* A name as it should appear inside an error message: quoted, and unambiguous whatever it contains.
|
|
82
|
+
*
|
|
83
|
+
* Almost every message this library throws names something the caller or the file chose - a sheet, a
|
|
84
|
+
* table, a defined name, a module stream, a part path - and the tree had grown three ways of setting
|
|
85
|
+
* that name off from the prose, split by directory rather than by intent: `"..."`, `'...'`, and
|
|
86
|
+
* `JSON.stringify`. Only the third survives a name that itself contains a quote, which is exactly the
|
|
87
|
+
* class of name a spreadsheet permits: a sheet called `Q1 "draft"` renders under either literal
|
|
88
|
+
* spelling as a message whose reader cannot tell where the name ends. It also renders a name
|
|
89
|
+
* containing a newline or a zero-width character as one that looks identical to a name that does not,
|
|
90
|
+
* which for a library whose input is untrusted is the difference between a diagnostic and a decoy.
|
|
91
|
+
*
|
|
92
|
+
* So: one spelling, and the one that escapes. `JSON.stringify` is the whole implementation - the
|
|
93
|
+
* point is not the algorithm but that every throw site reaches the same one.
|
|
94
|
+
*
|
|
95
|
+
* Not exported from the `/errors` entry: this is how messages are written, not part of the taxonomy
|
|
96
|
+
* a caller catches.
|
|
97
|
+
*/
|
|
98
|
+
export declare function quoted(name: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* The one message for "that token is not in the enumeration", owned here because both sides of a
|
|
101
|
+
* layering boundary throw it.
|
|
102
|
+
*
|
|
103
|
+
* `checkedToken` in `src/xml/xml.ts` is the general form, and `core/` may not import a serialisation
|
|
104
|
+
* to reach it. The response to that constraint had been to transcribe the sentence into
|
|
105
|
+
* `core/table-style.ts` character for character, which is a copy that no gate can see is a copy. This
|
|
106
|
+
* module is the one place below both of them, so the template lives here and neither side owns it.
|
|
107
|
+
*/
|
|
108
|
+
export declare function invalidToken(kind: string, value: string): AuthoringError;
|
|
109
|
+
/**
|
|
110
|
+
* Refuse a number OOXML cannot spell. Every numeric attribute in the format is `xsd:double`,
|
|
111
|
+
* `xsd:unsignedInt` or a bounded flavour of one, and none of those lexical spaces has a form for a
|
|
112
|
+
* NaN or an infinity, so a value that reaches the file as `NaN` is a package Excel reports as
|
|
113
|
+
* damaged. Neither has the formula grammar, which is why the guard is here rather than in the XML
|
|
114
|
+
* serialiser: the two spellings of a number this library writes -- an attribute's and a formula
|
|
115
|
+
* literal's -- sit on opposite sides of the `core`/`xml` boundary and refuse the same values.
|
|
116
|
+
*
|
|
117
|
+
* Exported for the callers that must refuse before they write. A number can be unwritable and still
|
|
118
|
+
* be read on the way to the bytes: compared, summed, walked. A comparison against `NaN` or an
|
|
119
|
+
* infinity silently takes the wrong branch long before the value would have been serialised.
|
|
120
|
+
*
|
|
121
|
+
* @throws {AuthoringError} naming the value.
|
|
122
|
+
*/
|
|
123
|
+
export declare function assertWritableNumber(value: number): void;
|
|
124
|
+
/** `U+0001`-style spelling of a code point, for an escape body or an error message. */
|
|
125
|
+
export declare function codePointHex(codePoint: number): string;
|
|
126
|
+
/**
|
|
127
|
+
* The error for a string carrying a character the target format cannot encode, or `undefined` when
|
|
128
|
+
* it carries none. Naming the code point and its offset is the whole value of the message: these
|
|
129
|
+
* strings arrive from a database column or a CSV field, not from a literal the author can see.
|
|
130
|
+
*
|
|
131
|
+
* Two formats refuse characters for unrelated reasons -- XML 1.0's `Char` production, and UTF-8's
|
|
132
|
+
* inability to encode a lone surrogate -- and each had grown its own copy of the rendering, which
|
|
133
|
+
* had already drifted: the CSV one omitted the zero padding, invisibly, because a surrogate is
|
|
134
|
+
* always four digits and nothing narrower had been added yet. The *patterns* stay beside their
|
|
135
|
+
* consumers, which is where they are understood; only the spelling and the message skeleton are
|
|
136
|
+
* shared, which is where they were wrong.
|
|
137
|
+
*
|
|
138
|
+
* `pattern` must not carry the `g` flag: a stateful `lastIndex` would make the same string answer
|
|
139
|
+
* differently on a second call.
|
|
140
|
+
*
|
|
141
|
+
* @param why the clause explaining the refusal, appended after the offset.
|
|
142
|
+
*/
|
|
143
|
+
export declare function unrepresentable(text: string, pattern: RegExp, why: string): AuthoringError | undefined;
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hex } from './hex.js';
|
|
1
2
|
export class XlsxError extends Error {
|
|
2
3
|
}
|
|
3
4
|
export class AuthoringError extends XlsxError {
|
|
@@ -13,3 +14,24 @@ export class InternalError extends XlsxError {
|
|
|
13
14
|
super(message === undefined ? REPORT_NOTICE : `${message}\n\n${REPORT_NOTICE}`, options);
|
|
14
15
|
}
|
|
15
16
|
}
|
|
17
|
+
export function quoted(name) {
|
|
18
|
+
return JSON.stringify(name);
|
|
19
|
+
}
|
|
20
|
+
export function invalidToken(kind, value) {
|
|
21
|
+
return new AuthoringError(`invalid ${kind} ${quoted(value)}: not a value the OOXML enumeration allows`);
|
|
22
|
+
}
|
|
23
|
+
export function assertWritableNumber(value) {
|
|
24
|
+
if (Number.isFinite(value))
|
|
25
|
+
return;
|
|
26
|
+
throw new AuthoringError(`cannot write a non-finite number (${value}): it has no OOXML representation`);
|
|
27
|
+
}
|
|
28
|
+
export function codePointHex(codePoint) {
|
|
29
|
+
return hex(codePoint, 4);
|
|
30
|
+
}
|
|
31
|
+
export function unrepresentable(text, pattern, why) {
|
|
32
|
+
const found = pattern.exec(text);
|
|
33
|
+
if (found === null)
|
|
34
|
+
return undefined;
|
|
35
|
+
const codePoint = text.codePointAt(found.index);
|
|
36
|
+
return new AuthoringError(`cannot write U+${codePointHex(codePoint)} at offset ${found.index}: ${why}`);
|
|
37
|
+
}
|
package/dist/hex.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `value` in uppercase hexadecimal, zero-padded to at least `digits`.
|
|
3
|
+
*
|
|
4
|
+
* Uppercase because five of the six sites this replaced were, because OOXML writes `ARGB` values that
|
|
5
|
+
* way, and because a hex digit read by a human beside a decimal number is easier to tell apart when
|
|
6
|
+
* it is not lowercase. A value wider than `digits` is not truncated: padding is a minimum.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hex(value: number, digits: number): string;
|
package/dist/hex.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ErrorCode } from '../../core/value.ts';
|
|
2
|
+
/** The metadata type whose blocks mark a dynamic-array formula. */
|
|
3
|
+
export declare const DYNAMIC_ARRAY_TYPE = "XLDAPR";
|
|
4
|
+
/** The metadata type whose blocks name a rich value. */
|
|
5
|
+
export declare const RICH_VALUE_TYPE = "XLRICHVALUE";
|
|
6
|
+
/** One record of a metadata block: `type` indexes the metadata types from 1, `value` that type's blocks from 0. */
|
|
7
|
+
export interface MetadataRecord {
|
|
8
|
+
readonly type: number;
|
|
9
|
+
readonly value: number;
|
|
10
|
+
}
|
|
11
|
+
/** The metadata part, read as far as a cell's `cm` and `vm` can reach into it. */
|
|
12
|
+
export interface WorkbookMetadata {
|
|
13
|
+
/** Each metadata type's name, in order. */
|
|
14
|
+
readonly typeNames: readonly string[];
|
|
15
|
+
/** Whether each dynamic-array block, in order, sets `fDynamic`. */
|
|
16
|
+
readonly dynamicArrayBlocks: readonly boolean[];
|
|
17
|
+
/** The rich value each rich-value block names, in order; -1 for a block naming none it can read. */
|
|
18
|
+
readonly richValueBlocks: readonly number[];
|
|
19
|
+
/** Each cell-metadata block's records, in order: a `cm` of 1 is the first. */
|
|
20
|
+
readonly cellBlocks: readonly (readonly MetadataRecord[])[];
|
|
21
|
+
/** Each value-metadata block's records, in order: a `vm` of 1 is the first. */
|
|
22
|
+
readonly valueBlocks: readonly (readonly MetadataRecord[])[];
|
|
23
|
+
}
|
|
24
|
+
/** A workbook with no metadata part. */
|
|
25
|
+
export declare const NO_METADATA: WorkbookMetadata;
|
|
26
|
+
/** What a cell's metadata indices resolve to, for the readers of every sheet in the workbook. */
|
|
27
|
+
export interface CellMetadataIndex {
|
|
28
|
+
/** The `cm` values marking a dynamic-array formula. */
|
|
29
|
+
readonly dynamicArrayCells: ReadonlySet<number>;
|
|
30
|
+
/** The error each `vm` names through a rich value, where it names one. */
|
|
31
|
+
readonly valueErrors: ReadonlyMap<number, ErrorCode>;
|
|
32
|
+
}
|
|
33
|
+
/** The index of a workbook with no metadata part: no cell is a dynamic array, and no value names an error. */
|
|
34
|
+
export declare const NO_CELL_METADATA: CellMetadataIndex;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve every `cm` and `vm` the metadata part can answer. `richValueErrors` is the error each rich
|
|
37
|
+
* value holds, by its index in the rich-value part.
|
|
38
|
+
*
|
|
39
|
+
* An index resolving to no block, to a block of another type, or to a rich value that is not an error
|
|
40
|
+
* marks nothing, so a cell carrying it reads as its own `<v>` says.
|
|
41
|
+
*/
|
|
42
|
+
export declare function indexCellMetadata(metadata: WorkbookMetadata, richValueErrors: ReadonlyMap<number, ErrorCode>): CellMetadataIndex;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const DYNAMIC_ARRAY_TYPE = 'XLDAPR';
|
|
2
|
+
export const RICH_VALUE_TYPE = 'XLRICHVALUE';
|
|
3
|
+
export const NO_METADATA = {
|
|
4
|
+
typeNames: [],
|
|
5
|
+
dynamicArrayBlocks: [],
|
|
6
|
+
richValueBlocks: [],
|
|
7
|
+
cellBlocks: [],
|
|
8
|
+
valueBlocks: [],
|
|
9
|
+
};
|
|
10
|
+
export const NO_CELL_METADATA = {
|
|
11
|
+
dynamicArrayCells: new Set(),
|
|
12
|
+
valueErrors: new Map(),
|
|
13
|
+
};
|
|
14
|
+
export function indexCellMetadata(metadata, richValueErrors) {
|
|
15
|
+
const typeOf = (record) => metadata.typeNames[record.type - 1];
|
|
16
|
+
const dynamicArrayCells = new Set();
|
|
17
|
+
metadata.cellBlocks.forEach((records, index) => {
|
|
18
|
+
const dynamic = records.some((record) => typeOf(record) === DYNAMIC_ARRAY_TYPE && metadata.dynamicArrayBlocks[record.value] === true);
|
|
19
|
+
if (dynamic)
|
|
20
|
+
dynamicArrayCells.add(index + 1);
|
|
21
|
+
});
|
|
22
|
+
const valueErrors = new Map();
|
|
23
|
+
metadata.valueBlocks.forEach((records, index) => {
|
|
24
|
+
for (const record of records) {
|
|
25
|
+
if (typeOf(record) !== RICH_VALUE_TYPE)
|
|
26
|
+
continue;
|
|
27
|
+
const error = richValueErrors.get(metadata.richValueBlocks[record.value] ?? -1);
|
|
28
|
+
if (error !== undefined) {
|
|
29
|
+
valueErrors.set(index + 1, error);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return { dynamicArrayCells, valueErrors };
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ErrorCode } from '../../core/value.ts';
|
|
2
|
+
/** The structure type of a rich value naming an error. */
|
|
3
|
+
export declare const ERROR_STRUCTURE = "_error";
|
|
4
|
+
/** The `errorType` a rich value names each error with, for the errors Excel stores only this way. */
|
|
5
|
+
export declare const RICH_VALUE_ERROR_TYPES: ReadonlyMap<ErrorCode, number>;
|
|
6
|
+
/**
|
|
7
|
+
* The error each rich value holds, keyed by its 0-based index in the rich-value part. A value whose
|
|
8
|
+
* structure is not `_error`, has no `errorType` key, or names an `errorType` Excel was not seen to read
|
|
9
|
+
* back as one error holds none.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseRichValueErrors(structuresXml: string, valuesXml: string): ReadonlyMap<number, ErrorCode>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { numInteger } from '../../xml/xml-attrs.js';
|
|
2
|
+
import { parseXml } from '../../xml/xml-read.js';
|
|
3
|
+
import { localName } from '../../xml/xml-scan.js';
|
|
4
|
+
export const ERROR_STRUCTURE = '_error';
|
|
5
|
+
export const RICH_VALUE_ERROR_TYPES = new Map([
|
|
6
|
+
['#SPILL!', 8],
|
|
7
|
+
['#CONNECT!', 9],
|
|
8
|
+
['#BLOCKED!', 10],
|
|
9
|
+
['#UNKNOWN!', 11],
|
|
10
|
+
['#FIELD!', 12],
|
|
11
|
+
['#CALC!', 13],
|
|
12
|
+
]);
|
|
13
|
+
const ERROR_OF_TYPE = new Map([
|
|
14
|
+
[3, '#REF!'],
|
|
15
|
+
[4, '#NAME?'],
|
|
16
|
+
[6, '#N/A'],
|
|
17
|
+
...[...RICH_VALUE_ERROR_TYPES].map(([code, type]) => [type, code]),
|
|
18
|
+
]);
|
|
19
|
+
export function parseRichValueErrors(structuresXml, valuesXml) {
|
|
20
|
+
const errors = new Map();
|
|
21
|
+
if (structuresXml === '' || valuesXml === '')
|
|
22
|
+
return errors;
|
|
23
|
+
const errorTypeKeys = parseErrorTypeKeys(structuresXml);
|
|
24
|
+
let index = -1;
|
|
25
|
+
let wanted = -1;
|
|
26
|
+
let position = -1;
|
|
27
|
+
let capturing = false;
|
|
28
|
+
let text = '';
|
|
29
|
+
parseXml(valuesXml, {
|
|
30
|
+
onOpen(name, attrs, selfClosing) {
|
|
31
|
+
const local = localName(name);
|
|
32
|
+
if (local === 'rv') {
|
|
33
|
+
index++;
|
|
34
|
+
position = -1;
|
|
35
|
+
const structure = numInteger(attrs.s, 0);
|
|
36
|
+
wanted = structure === undefined ? -1 : (errorTypeKeys[structure] ?? -1);
|
|
37
|
+
}
|
|
38
|
+
else if (local === 'v') {
|
|
39
|
+
position++;
|
|
40
|
+
capturing = !selfClosing && position === wanted;
|
|
41
|
+
text = '';
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
onText(chunk) {
|
|
45
|
+
if (capturing)
|
|
46
|
+
text += chunk;
|
|
47
|
+
},
|
|
48
|
+
onClose(name) {
|
|
49
|
+
if (localName(name) !== 'v' || !capturing)
|
|
50
|
+
return;
|
|
51
|
+
capturing = false;
|
|
52
|
+
const error = ERROR_OF_TYPE.get(numInteger(text.trim(), 0) ?? -1);
|
|
53
|
+
if (error !== undefined)
|
|
54
|
+
errors.set(index, error);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return errors;
|
|
58
|
+
}
|
|
59
|
+
function parseErrorTypeKeys(xml) {
|
|
60
|
+
const keys = [];
|
|
61
|
+
let isError = false;
|
|
62
|
+
let key = -1;
|
|
63
|
+
parseXml(xml, {
|
|
64
|
+
onOpen(name, attrs) {
|
|
65
|
+
const local = localName(name);
|
|
66
|
+
if (local === 's') {
|
|
67
|
+
isError = attrs.t === ERROR_STRUCTURE;
|
|
68
|
+
key = -1;
|
|
69
|
+
keys.push(-1);
|
|
70
|
+
}
|
|
71
|
+
else if (local === 'k' && keys.length > 0) {
|
|
72
|
+
key++;
|
|
73
|
+
if (isError && attrs.n === 'errorType' && keys[keys.length - 1] === -1) {
|
|
74
|
+
keys[keys.length - 1] = key;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return keys;
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse a field delimiter the codec cannot honour.
|
|
3
|
+
*
|
|
4
|
+
* A `RangeError` rather than an `AuthoringError`: this is a single scalar out of range, which
|
|
5
|
+
* `src/errors.ts` reserves for the native types on the grounds that they exist for exactly that.
|
|
6
|
+
*
|
|
7
|
+
* @throws {RangeError} if the delimiter is not exactly one character, or is a quote, CR or LF.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertDelimiter(delimiter: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Refuse a row delimiter the writer cannot keep apart from the data.
|
|
12
|
+
*
|
|
13
|
+
* Only the writer takes one: the reader recognises LF, CRLF and a bare CR, and a record boundary it
|
|
14
|
+
* cannot see is a choice the caller makes for a consumer other than this codec. What is refused here
|
|
15
|
+
* is output no consumer can split. An empty delimiter joins every row into one line. One that
|
|
16
|
+
* contains the field delimiter reads as an extra field at every row boundary, and one that contains a
|
|
17
|
+
* quote collides with the quoting that is supposed to protect a field from it.
|
|
18
|
+
*
|
|
19
|
+
* @throws {RangeError} if the row delimiter is empty, or contains the field delimiter or a quote.
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertRowDelimiter(rowDelimiter: string, delimiter: string): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { quoted } from '../../errors.js';
|
|
2
|
+
const RESERVED = new Set(['"', '\r', '\n']);
|
|
3
|
+
export function assertDelimiter(delimiter) {
|
|
4
|
+
if (delimiter.length !== 1) {
|
|
5
|
+
throw new RangeError(`CSV delimiter must be a single character, got ${quoted(delimiter)}`);
|
|
6
|
+
}
|
|
7
|
+
if (RESERVED.has(delimiter)) {
|
|
8
|
+
throw new RangeError(`CSV delimiter ${quoted(delimiter)} is reserved: a quote opens a quoted field, and CR and LF end a row`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function assertRowDelimiter(rowDelimiter, delimiter) {
|
|
12
|
+
if (rowDelimiter === '') {
|
|
13
|
+
throw new RangeError('CSV row delimiter must not be empty');
|
|
14
|
+
}
|
|
15
|
+
if (rowDelimiter.includes(delimiter) || rowDelimiter.includes('"')) {
|
|
16
|
+
throw new RangeError(`CSV row delimiter ${quoted(rowDelimiter)} must contain neither the field delimiter ${quoted(delimiter)} nor a quote`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { XlsxError } from '../../errors.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when CSV text parses but cannot become a worksheet: a record with more fields than a sheet
|
|
4
|
+
* has columns, or more records than it has rows.
|
|
5
|
+
*
|
|
6
|
+
* CSV has no malformed markup to reject, since any text splits into fields somehow, so the only way a
|
|
7
|
+
* file fails is by not fitting the grid. That is still the file's fault rather than the caller's,
|
|
8
|
+
* which is why it is this error and not the `RangeError` the grid raises for an authored cell out of
|
|
9
|
+
* bounds.
|
|
10
|
+
*/
|
|
11
|
+
export declare class CsvParseError extends XlsxError {
|
|
12
|
+
readonly name = "CsvParseError";
|
|
13
|
+
readonly code = "malformed-input";
|
|
14
|
+
}
|
package/dist/io/csv/read.d.ts
CHANGED
|
@@ -11,5 +11,10 @@ export interface CsvReadOptions {
|
|
|
11
11
|
/** Name for the single worksheet produced; defaults to `"Sheet1"`. */
|
|
12
12
|
readonly sheetName?: string;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Parse CSV text (or UTF-8 bytes) into a workbook holding a single worksheet.
|
|
16
|
+
*
|
|
17
|
+
* @throws {CsvParseError} if a record has more fields than a worksheet has columns, or the data
|
|
18
|
+
* records outnumber a worksheet's rows.
|
|
19
|
+
*/
|
|
15
20
|
export declare function readCsv(input: string | Uint8Array, options?: CsvReadOptions): Workbook;
|
package/dist/io/csv/read.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { MAX_COLUMN, MAX_ROW } from '../../core/address.js';
|
|
1
2
|
import { Workbook } from '../../core/workbook.js';
|
|
3
|
+
import { assertDelimiter } from './delimiter.js';
|
|
4
|
+
import { CsvParseError } from './errors.js';
|
|
2
5
|
export function readCsv(input, options = {}) {
|
|
3
6
|
const text = stripBom(typeof input === 'string' ? input : new TextDecoder('utf-8', { ignoreBOM: true }).decode(input));
|
|
4
7
|
const delimiter = options.delimiter ?? ',';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
let rows = parseCsvRows(text, delimiter);
|
|
8
|
+
assertDelimiter(delimiter);
|
|
9
|
+
let rows = parseCsvRows(text, delimiter, MAX_ROW + (options.headers ? 1 : 0));
|
|
9
10
|
if (options.headers)
|
|
10
11
|
rows = rows.slice(1);
|
|
11
12
|
const coerce = options.map ?? defaultCsvCoerce;
|
|
@@ -17,11 +18,28 @@ export function readCsv(input, options = {}) {
|
|
|
17
18
|
function stripBom(text) {
|
|
18
19
|
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
19
20
|
}
|
|
20
|
-
function parseCsvRows(text, delimiter) {
|
|
21
|
+
function parseCsvRows(text, delimiter, maxRecords) {
|
|
21
22
|
const rows = [];
|
|
22
23
|
let row = [];
|
|
23
24
|
let field = '';
|
|
24
25
|
let inQuotes = false;
|
|
26
|
+
let atFieldStart = true;
|
|
27
|
+
const endField = () => {
|
|
28
|
+
if (row.length === MAX_COLUMN) {
|
|
29
|
+
throw new CsvParseError(`CSV record ${rows.length + 1} has more than ${MAX_COLUMN} fields, the most columns a worksheet holds`);
|
|
30
|
+
}
|
|
31
|
+
row.push(field);
|
|
32
|
+
field = '';
|
|
33
|
+
atFieldStart = true;
|
|
34
|
+
};
|
|
35
|
+
const endRow = () => {
|
|
36
|
+
endField();
|
|
37
|
+
if (rows.length === maxRecords) {
|
|
38
|
+
throw new CsvParseError(`CSV has more than ${MAX_ROW} data records, the most rows a worksheet holds`);
|
|
39
|
+
}
|
|
40
|
+
rows.push(row);
|
|
41
|
+
row = [];
|
|
42
|
+
};
|
|
25
43
|
for (let i = 0; i < text.length; i++) {
|
|
26
44
|
const ch = text.charAt(i);
|
|
27
45
|
if (inQuotes) {
|
|
@@ -39,37 +57,43 @@ function parseCsvRows(text, delimiter) {
|
|
|
39
57
|
}
|
|
40
58
|
continue;
|
|
41
59
|
}
|
|
42
|
-
if (ch === '"') {
|
|
60
|
+
if (ch === '"' && atFieldStart) {
|
|
43
61
|
inQuotes = true;
|
|
62
|
+
atFieldStart = false;
|
|
44
63
|
}
|
|
45
64
|
else if (ch === delimiter) {
|
|
46
|
-
|
|
47
|
-
field = '';
|
|
65
|
+
endField();
|
|
48
66
|
}
|
|
49
67
|
else if (ch === '\n') {
|
|
50
|
-
|
|
51
|
-
rows.push(row);
|
|
52
|
-
row = [];
|
|
53
|
-
field = '';
|
|
68
|
+
endRow();
|
|
54
69
|
}
|
|
55
|
-
else if (ch
|
|
70
|
+
else if (ch === '\r') {
|
|
71
|
+
if (text[i + 1] !== '\n')
|
|
72
|
+
endRow();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
56
75
|
field += ch;
|
|
76
|
+
atFieldStart = false;
|
|
57
77
|
}
|
|
58
78
|
}
|
|
59
|
-
row.push(field);
|
|
60
79
|
const trailingNewline = text.endsWith('\n') || text.endsWith('\r');
|
|
61
|
-
if (!(trailingNewline && row.length ===
|
|
62
|
-
|
|
80
|
+
if (!(trailingNewline && row.length === 0 && field === ''))
|
|
81
|
+
endRow();
|
|
63
82
|
return rows;
|
|
64
83
|
}
|
|
65
84
|
const NUMERIC = /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/;
|
|
66
85
|
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
86
|
+
const MAX_EXACT_SIGNIFICANT_DIGITS = 15;
|
|
67
87
|
function defaultCsvCoerce(field) {
|
|
68
88
|
if (field === '')
|
|
69
89
|
return null;
|
|
70
|
-
|
|
90
|
+
const numeric = NUMERIC.exec(field);
|
|
91
|
+
if (numeric) {
|
|
71
92
|
const value = Number(field);
|
|
72
|
-
|
|
93
|
+
const exact = Number.isFinite(value) &&
|
|
94
|
+
Math.abs(value) <= Number.MAX_SAFE_INTEGER &&
|
|
95
|
+
significantDigits(numeric[1] ?? '') <= MAX_EXACT_SIGNIFICANT_DIGITS;
|
|
96
|
+
return exact ? value : field;
|
|
73
97
|
}
|
|
74
98
|
const iso = ISO_DATE.exec(field);
|
|
75
99
|
if (iso) {
|
|
@@ -79,6 +103,9 @@ function defaultCsvCoerce(field) {
|
|
|
79
103
|
}
|
|
80
104
|
return field;
|
|
81
105
|
}
|
|
106
|
+
function significantDigits(mantissa) {
|
|
107
|
+
return mantissa.replace('.', '').replace(/^0+/, '').replace(/0+$/, '').length;
|
|
108
|
+
}
|
|
82
109
|
function isoToDate(match) {
|
|
83
110
|
const year = Number(match[1]);
|
|
84
111
|
const month = Number(match[2]);
|
|
@@ -86,9 +113,11 @@ function isoToDate(match) {
|
|
|
86
113
|
const hour = match[4] === undefined ? 0 : Number(match[4]);
|
|
87
114
|
const minute = match[5] === undefined ? 0 : Number(match[5]);
|
|
88
115
|
const second = match[6] === undefined ? 0 : Number(match[6]);
|
|
89
|
-
if (
|
|
116
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
90
117
|
return null;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
const date = new Date(0);
|
|
119
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
120
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
121
|
+
const real = date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
122
|
+
return real ? date : null;
|
|
94
123
|
}
|
package/dist/io/csv/write.d.ts
CHANGED
|
@@ -14,12 +14,20 @@ export interface CsvWriteOptions {
|
|
|
14
14
|
/** Which worksheet to write; defaults to the first. A name matching no sheet throws rather than
|
|
15
15
|
* silently emitting an empty file. */
|
|
16
16
|
readonly sheetName?: string;
|
|
17
|
-
/** Field separator; defaults to a comma. */
|
|
17
|
+
/** Field separator; defaults to a comma. A single character other than a quote, CR or LF. */
|
|
18
18
|
readonly delimiter?: string;
|
|
19
|
-
/** Line separator between rows; defaults to `"\n"`.
|
|
19
|
+
/** Line separator between rows; defaults to `"\n"`. Not empty, and containing neither the field
|
|
20
|
+
* delimiter nor a quote. A field that contains it is quoted. */
|
|
20
21
|
readonly rowDelimiter?: string;
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
22
|
+
/**
|
|
23
|
+
* An Excel number-format code (e.g. `"yyyy-mm-dd"`, `"d mmm yy hh:mm"`) for Date cells; without it
|
|
24
|
+
* a Date renders as a full ISO-8601 timestamp.
|
|
25
|
+
*
|
|
26
|
+
* The same vocabulary as {@link Cell.numFmt}, so `writeCsv(wb, {dateFormat: cell.numFmt})` renders
|
|
27
|
+
* what the cell would show. It used to be a moment.js-style token set, which is case-sensitive and
|
|
28
|
+
* spells the month `MM` and the minute `mm`: passing this library's own format codes to it produced
|
|
29
|
+
* `2024-45-dd` with no throw and no warning. See ADR 0041.
|
|
30
|
+
*/
|
|
23
31
|
readonly dateFormat?: string;
|
|
24
32
|
/** Render Date cells in UTC rather than the runner's local time. */
|
|
25
33
|
readonly dateUTC?: boolean;
|
|
@@ -32,12 +40,18 @@ export interface CsvWriteOptions {
|
|
|
32
40
|
* still applied to the returned text. */
|
|
33
41
|
readonly map?: (value: CellValue, index: number) => string;
|
|
34
42
|
}
|
|
35
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The logical CSV text of one worksheet: no BOM, no byte encoding. Line N holds row N, so an empty
|
|
45
|
+
* row before the last populated one is an empty line.
|
|
46
|
+
*
|
|
47
|
+
* @throws {RangeError} if `delimiter` or `rowDelimiter` is one the output could not be split on.
|
|
48
|
+
*/
|
|
36
49
|
export declare function writeCsvText(workbook: Workbook, options?: CsvWriteOptions): string;
|
|
37
50
|
/**
|
|
38
51
|
* The CSV bytes of one worksheet in the requested encoding, with a UTF-8 BOM by default.
|
|
39
52
|
*
|
|
40
53
|
* @throws {AuthoringError} if a field holds an unpaired surrogate and the encoding is UTF-8, which
|
|
41
54
|
* cannot represent one. The alternative is a silent U+FFFD substitution.
|
|
55
|
+
* @throws {RangeError} on a delimiter {@link writeCsvText} refuses.
|
|
42
56
|
*/
|
|
43
57
|
export declare function writeCsv(workbook: Workbook, options?: CsvWriteOptions): Uint8Array;
|
package/dist/io/csv/write.js
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { formatSerialDate } from '../../core/date-format.js';
|
|
2
|
+
import { cellValueToText, isArrayFormulaValue, isDataTableFormulaValue, isFormulaValue, isSharedFormulaValue, } from '../../core/value.js';
|
|
3
|
+
import { AuthoringError, unrepresentable } from '../../errors.js';
|
|
4
|
+
import { assertDelimiter, assertRowDelimiter } from './delimiter.js';
|
|
3
5
|
const UTF8_BOM = Uint8Array.of(0xef, 0xbb, 0xbf);
|
|
4
6
|
export function writeCsvText(workbook, options = {}) {
|
|
5
7
|
const sheet = selectSheet(workbook, options.sheetName);
|
|
6
8
|
const delimiter = options.delimiter ?? ',';
|
|
9
|
+
assertDelimiter(delimiter);
|
|
7
10
|
const rowDelimiter = options.rowDelimiter ?? '\n';
|
|
11
|
+
assertRowDelimiter(rowDelimiter, delimiter);
|
|
8
12
|
const lines = [];
|
|
9
|
-
for (const
|
|
13
|
+
for (const row of sheet.rows()) {
|
|
10
14
|
let width = 0;
|
|
11
15
|
const byColumn = new Map();
|
|
12
|
-
for (const cell of cells) {
|
|
16
|
+
for (const cell of row.cells) {
|
|
13
17
|
byColumn.set(cell.col, cell);
|
|
14
18
|
if (cell.col > width)
|
|
15
19
|
width = cell.col;
|
|
16
20
|
}
|
|
21
|
+
if (width === 0)
|
|
22
|
+
continue;
|
|
17
23
|
const fields = [];
|
|
18
24
|
for (let column = 1; column <= width; column++) {
|
|
19
25
|
const value = byColumn.get(column)?.value ?? null;
|
|
20
26
|
const text = options.map ? options.map(value, column - 1) : csvFieldText(value, options);
|
|
21
|
-
fields.push(quoteField(text, delimiter));
|
|
27
|
+
fields.push(quoteField(text, delimiter, rowDelimiter));
|
|
22
28
|
}
|
|
29
|
+
while (lines.length < row.number - 1)
|
|
30
|
+
lines.push('');
|
|
23
31
|
lines.push(fields.join(delimiter));
|
|
24
32
|
}
|
|
25
33
|
return lines.join(rowDelimiter);
|
|
@@ -58,14 +66,11 @@ function encode(text, encoding) {
|
|
|
58
66
|
return bytes;
|
|
59
67
|
}
|
|
60
68
|
function assertEncodable(text) {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
const codePoint = text.codePointAt(found.index).toString(16).toUpperCase();
|
|
65
|
-
throw new AuthoringError(`cannot write U+${codePoint} at offset ${found.index} of the CSV text: it is an unpaired ` +
|
|
66
|
-
'surrogate, which UTF-8 cannot encode and CSV has no escape for');
|
|
69
|
+
const error = unrepresentable(text, SURROGATES, 'it is an unpaired surrogate, which UTF-8 cannot encode and CSV has no escape for');
|
|
70
|
+
if (error !== undefined)
|
|
71
|
+
throw error;
|
|
67
72
|
}
|
|
68
|
-
const
|
|
73
|
+
const SURROGATES = /[\u{D800}-\u{DFFF}]/u;
|
|
69
74
|
function selectSheet(workbook, name) {
|
|
70
75
|
if (name === undefined) {
|
|
71
76
|
const first = workbook.worksheets[0];
|
|
@@ -77,44 +82,19 @@ function selectSheet(workbook, name) {
|
|
|
77
82
|
}
|
|
78
83
|
function csvFieldText(value, options) {
|
|
79
84
|
if (value instanceof Date && options.dateFormat !== undefined) {
|
|
80
|
-
return
|
|
85
|
+
return formatSerialDate(value, options.dateFormat, options.dateUTC ?? false);
|
|
81
86
|
}
|
|
82
|
-
if (isFormulaValue(value) ||
|
|
87
|
+
if (isFormulaValue(value) ||
|
|
88
|
+
isSharedFormulaValue(value) ||
|
|
89
|
+
isArrayFormulaValue(value) ||
|
|
90
|
+
isDataTableFormulaValue(value)) {
|
|
83
91
|
return value.result === undefined ? '' : csvFieldText(value.result, options);
|
|
84
92
|
}
|
|
85
93
|
return cellValueToText(value);
|
|
86
94
|
}
|
|
87
|
-
|
|
88
|
-
function formatDate(date, format, utc) {
|
|
89
|
-
if (Number.isNaN(date.getTime()))
|
|
90
|
-
return '';
|
|
91
|
-
if (format === undefined)
|
|
92
|
-
return date.toISOString();
|
|
93
|
-
const year = utc ? date.getUTCFullYear() : date.getFullYear();
|
|
94
|
-
const month = (utc ? date.getUTCMonth() : date.getMonth()) + 1;
|
|
95
|
-
const day = utc ? date.getUTCDate() : date.getDate();
|
|
96
|
-
const hour = utc ? date.getUTCHours() : date.getHours();
|
|
97
|
-
const minute = utc ? date.getUTCMinutes() : date.getMinutes();
|
|
98
|
-
const second = utc ? date.getUTCSeconds() : date.getSeconds();
|
|
99
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
100
|
-
const tokens = {
|
|
101
|
-
YYYY: String(year),
|
|
102
|
-
YY: pad(year % 100),
|
|
103
|
-
MM: pad(month),
|
|
104
|
-
M: String(month),
|
|
105
|
-
DD: pad(day),
|
|
106
|
-
D: String(day),
|
|
107
|
-
HH: pad(hour),
|
|
108
|
-
H: String(hour),
|
|
109
|
-
mm: pad(minute),
|
|
110
|
-
m: String(minute),
|
|
111
|
-
ss: pad(second),
|
|
112
|
-
s: String(second),
|
|
113
|
-
};
|
|
114
|
-
return format.replace(DATE_TOKENS, (token) => tokens[token] ?? token);
|
|
115
|
-
}
|
|
116
|
-
function quoteField(field, delimiter) {
|
|
95
|
+
function quoteField(field, delimiter, rowDelimiter) {
|
|
117
96
|
if (field.includes(delimiter) ||
|
|
97
|
+
field.includes(rowDelimiter) ||
|
|
118
98
|
field.includes('"') ||
|
|
119
99
|
field.includes('\n') ||
|
|
120
100
|
field.includes('\r')) {
|