@eifi1/ui-kit 0.21.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +19 -14
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +53 -14
- package/dist/components/amount-input.js +17 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +29 -0
- package/dist/components/autocomplete.js +134 -104
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +19 -14
- package/dist/components/calculator.d.ts +19 -14
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +91 -2
- package/dist/components/combobox.js +454 -308
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +69 -161
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +16 -11
- package/dist/components/country-select.d.ts +167 -0
- package/dist/components/country-select.js +293 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +69 -166
- package/dist/components/danger-confirm.js +144 -50
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +66 -5
- package/dist/components/date-picker.js +378 -66
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +42 -1
- package/dist/components/entity-combobox.js +154 -113
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -14
- package/dist/components/field-parts.d.ts +224 -0
- package/dist/components/field-parts.js +156 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +19 -14
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +41 -15
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -13
- package/dist/components/form-actions.js +69 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +69 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +21 -2
- package/dist/components/icon-picker.js +19 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +130 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +32 -14
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +291 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +34 -19
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +79 -19
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +19 -14
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +69 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +19 -14
- package/dist/components/settings-fields.d.ts +19 -14
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +43 -2
- package/dist/components/swatch-picker.js +80 -32
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +18 -13
- package/dist/components/tile-radio.d.ts +88 -13
- package/dist/components/tile-radio.js +28 -11
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +24 -15
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +16 -11
- package/dist/components/ui.js +284 -88
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +348 -41
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -253
- package/dist/feedback/feedback-thread.js +46 -10
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +19 -14
- package/dist/i18n/defaults.d.ts +19 -14
- package/dist/i18n/defaults.js +19 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +19 -14
- package/dist/i18n/german.js +84 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -10
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +19 -14
- package/dist/i18n/locales/de-CH.d.ts +19 -14
- package/dist/i18n/locales/en.d.ts +19 -14
- package/dist/i18n/locales/en.js +16 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +19 -14
- package/dist/i18n/locales/es.js +83 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +19 -14
- package/dist/i18n/locales/fr.js +82 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +19 -14
- package/dist/i18n/locales/hu.js +85 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +19 -14
- package/dist/i18n/locales/it.js +84 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +19 -14
- package/dist/i18n/locales/zh.js +80 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +19 -14
- package/dist/i18n/review.js +27 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +233 -18
- package/dist/rhf/fields.js +456 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +19 -14
- package/dist/rhf.d.ts +22 -15
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +18 -13
- package/dist/shell/top-bar-brand.d.ts +19 -14
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +18 -13
- package/dist/table-text.d.ts +1 -1
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -16
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +19 -14
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +68 -56
- package/src/components/autocomplete.tsx +198 -114
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +661 -340
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +556 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +429 -82
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +595 -71
- package/src/components/entity-combobox.tsx +235 -124
- package/src/components/field-parts.tsx +403 -0
- package/src/components/field-strip.tsx +149 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +147 -6
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +47 -6
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +13 -0
- package/src/components/month-picker.tsx +400 -63
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +143 -31
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +145 -29
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +479 -138
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +597 -46
- package/src/feedback/feedback-dialog.tsx +9 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +113 -9
- package/src/i18n/defaults.ts +18 -1
- package/src/i18n/german.ts +89 -0
- package/src/i18n/kit-labels.tsx +82 -2
- package/src/i18n/locales/en.ts +36 -4
- package/src/i18n/locales/es.ts +87 -0
- package/src/i18n/locales/fr.ts +87 -0
- package/src/i18n/locales/hu.ts +83 -0
- package/src/i18n/locales/it.ts +88 -0
- package/src/i18n/locales/zh.ts +76 -0
- package/src/i18n/review.ts +26 -0
- package/src/index.ts +71 -3
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +860 -9
- package/src/rhf.ts +3 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/tour/tour.tsx +66 -28
- package/src/wizard/stepper-nav.tsx +35 -1
package/src/lib/table-text.ts
CHANGED
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
* * **Quoted fields.** No `"a;b"` escaping. These are numbers; the only text in them
|
|
47
47
|
* is the header line, and a header field with a separator in it costs a column
|
|
48
48
|
* name, not a measurement.
|
|
49
|
+
*
|
|
50
|
+
* Both of those are the NUMERIC lexer's limits. A table whose cells are text — a bank
|
|
51
|
+
* export, read for a column mapper — needs both, and {@link parseTextTable} (0.23) is
|
|
52
|
+
* that door: empty cells held in place, quoted cells, and the same separator and comma
|
|
53
|
+
* rules underneath.
|
|
49
54
|
*/
|
|
50
55
|
|
|
51
56
|
/** A digit, a comma, a digit — a comma that can only be a decimal mark or a
|
|
@@ -263,3 +268,269 @@ function fieldsOf(line: string, separator: Separator): string[] {
|
|
|
263
268
|
* field with two of them is a grouped number and this module does not read
|
|
264
269
|
* those — it reports the line instead. */
|
|
265
270
|
const point = (field: string) => field.replace(",", ".");
|
|
271
|
+
|
|
272
|
+
// ── A table of text ────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* What separates the columns of a {@link TextTable}, named. `" "` is a run of
|
|
276
|
+
* whitespace — one separator however many spaces or tabs it is made of — as in a
|
|
277
|
+
* printed or MATLAB-dumped table.
|
|
278
|
+
*/
|
|
279
|
+
export type TableSeparator = "\t" | ";" | "," | " ";
|
|
280
|
+
|
|
281
|
+
/** One line of the text as it was handed over: its 1-based number (blank lines
|
|
282
|
+
* counted, so a reader can count to it in the box they pasted into) and its text,
|
|
283
|
+
* trimmed. */
|
|
284
|
+
export interface TableLine {
|
|
285
|
+
line: number;
|
|
286
|
+
text: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface ParseTextTableOptions {
|
|
290
|
+
/** The separator, decided by the caller — a "separated by" override, a format
|
|
291
|
+
* the app already knows. Default: detected (see {@link parseTextTable}). */
|
|
292
|
+
separator?: TableSeparator;
|
|
293
|
+
/** Whether a comma between two digits is a decimal mark. Default: the
|
|
294
|
+
* `"whole-text"` rule, decided once for the file. */
|
|
295
|
+
decimalComma?: boolean;
|
|
296
|
+
/** Whether the table's first line names its columns. Default: detected — a
|
|
297
|
+
* word over a column of values ("Amount" over `-12,50`) is a header. */
|
|
298
|
+
header?: boolean;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A table read as text: every cell a string, as written.
|
|
303
|
+
*
|
|
304
|
+
* The *file* door's decisions, for a table that is not only numbers. Everything a
|
|
305
|
+
* reader has to know to trust it is in the answer — the separator and the decimal
|
|
306
|
+
* mark that were assumed, and the lines that were not read, with their number and
|
|
307
|
+
* their text — because a guess nobody can see is a guess nobody can correct.
|
|
308
|
+
*/
|
|
309
|
+
export interface TextTable {
|
|
310
|
+
/** The first line's cells, when it names the columns; `null` when it is data. */
|
|
311
|
+
header: string[] | null;
|
|
312
|
+
/** Each read row, `width` cells long. An empty cell is `""`, kept in its place. */
|
|
313
|
+
rows: string[][];
|
|
314
|
+
/** Where each row came from — row-aligned with `rows`. */
|
|
315
|
+
lines: TableLine[];
|
|
316
|
+
/** How many columns the table has. */
|
|
317
|
+
width: number;
|
|
318
|
+
separator: TableSeparator;
|
|
319
|
+
/** Whether a comma is this table's decimal mark — the convention its numbers are
|
|
320
|
+
* written in, to read them with {@link tableNumber} and to say back to the reader. */
|
|
321
|
+
decimalComma: boolean;
|
|
322
|
+
/** Lines that are not a row of this table: a title above it, a total under it, a
|
|
323
|
+
* row with a cell too many or too few. */
|
|
324
|
+
unread: TableLine[];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* A pasted or dropped table, read as text — the lexer's file door for a table whose
|
|
329
|
+
* cells are not all numbers (a bank export: a date, a payee, an amount), and the
|
|
330
|
+
* parse under {@link ColumnMapper}.
|
|
331
|
+
*
|
|
332
|
+
* Moved under the same roof as {@link parseTable} because it has the same three
|
|
333
|
+
* questions — what separates the columns, what the comma is, whether the first
|
|
334
|
+
* line is a header — and answering them a second time somewhere else is how the
|
|
335
|
+
* three readers this module replaced came to disagree. Kurvenschmiede's columns
|
|
336
|
+
* input and keksdose's bank-file import both read a table whose columns the reader
|
|
337
|
+
* then assigns, and both state what was assumed; this is that reading, once.
|
|
338
|
+
*
|
|
339
|
+
* Where it differs from {@link parseTable}, and why:
|
|
340
|
+
*
|
|
341
|
+
* * **Cells stay text.** Which cell is a number is the caller's question (its
|
|
342
|
+
* column roles), so nothing here converts. {@link tableNumber} reads one in the
|
|
343
|
+
* table's own convention.
|
|
344
|
+
* * **An empty cell keeps its place.** A bank row with no memo has `;;` in it, and
|
|
345
|
+
* dropping that field — which the numeric lexer does — would move the amount into
|
|
346
|
+
* the memo's column. A trailing separator that ends most lines (`1;2;3;`, which
|
|
347
|
+
* spreadsheets write) is a line ending, not a column.
|
|
348
|
+
* * **Quoted cells.** `"Example Ltd; branch 2"` is one cell, and `""` inside quotes
|
|
349
|
+
* is a quote. Text has separators in it where numbers never do. A quoted cell that
|
|
350
|
+
* runs over a line break is not joined — the line then has the wrong number of
|
|
351
|
+
* cells and is reported.
|
|
352
|
+
* * **The width is the file's.** The column count most lines share. A line with
|
|
353
|
+
* another count is reported in `unread` rather than padded or cut: a row with a
|
|
354
|
+
* cell too many is a row whose columns have slid, and reading it would put a payee
|
|
355
|
+
* in the amount.
|
|
356
|
+
* * **The separator is voted on**, over the first lines, rather than taken from the
|
|
357
|
+
* second: an export that opens with a title block (a bank's account name and
|
|
358
|
+
* period over the table) would otherwise be read with the title's separator.
|
|
359
|
+
* * **The header is detected by its words.** The numeric lexer's rule — "a line
|
|
360
|
+
* that is not numbers" — cannot work where the rows are text too. A first line is
|
|
361
|
+
* a header when one of its cells is a word and every cell under it, as far as the
|
|
362
|
+
* first rows go, is a value (digits and the marks numbers and dates are written
|
|
363
|
+
* with): "Amount" over `-12,50`, "Date" over `01.02.2026`. A table of words only
|
|
364
|
+
* is read without one; `options.header` says otherwise.
|
|
365
|
+
*
|
|
366
|
+
* Blank lines and `#` banners are skipped, as by the numeric lexer.
|
|
367
|
+
*/
|
|
368
|
+
export function parseTextTable(text: string, options: ParseTextTableOptions = {}): TextTable {
|
|
369
|
+
const content = textLines(text);
|
|
370
|
+
if (!content.length) {
|
|
371
|
+
return { header: null, rows: [], lines: [], width: 0, separator: options.separator ?? ",", decimalComma: false, unread: [] };
|
|
372
|
+
}
|
|
373
|
+
const separator = options.separator ?? votedSeparator(content);
|
|
374
|
+
const decimalComma =
|
|
375
|
+
options.decimalComma ?? (separator !== "," && DECIMAL_COMMA.test(content.map((entry) => entry.line).join("\n")));
|
|
376
|
+
|
|
377
|
+
// A trailing separator is a line ending only where it is the file's habit: in a
|
|
378
|
+
// file where most lines end in one. Elsewhere it closes an empty last cell.
|
|
379
|
+
const trailing =
|
|
380
|
+
separator !== " " && content.filter(({ line }) => line.endsWith(separator)).length * 2 > content.length;
|
|
381
|
+
const split = content.map(({ line, number }) => {
|
|
382
|
+
const cells = cellsOf(line, separator);
|
|
383
|
+
if (trailing && line.endsWith(separator) && cells[cells.length - 1] === "") cells.pop();
|
|
384
|
+
return { cells, entry: { line: number, text: line } };
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const width = commonWidth(split.map(({ cells }) => cells.length));
|
|
388
|
+
const first = split.findIndex(({ cells }) => cells.length === width);
|
|
389
|
+
const unread: TableLine[] = split.slice(0, first).map(({ entry }) => entry);
|
|
390
|
+
const body = split.slice(first);
|
|
391
|
+
const fitting = body.filter(({ cells }) => cells.length === width);
|
|
392
|
+
const header = options.header ?? looksLikeHeader(fitting[0].cells, fitting.slice(1, 21).map(({ cells }) => cells));
|
|
393
|
+
|
|
394
|
+
const rows: string[][] = [];
|
|
395
|
+
const lines: TableLine[] = [];
|
|
396
|
+
body.forEach(({ cells, entry }, index) => {
|
|
397
|
+
if (index === 0 && header) return;
|
|
398
|
+
if (cells.length !== width) {
|
|
399
|
+
unread.push(entry);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
rows.push(cells);
|
|
403
|
+
lines.push(entry);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return { header: header ? body[0].cells : null, rows, lines, width, separator, decimalComma, unread };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* One cell of a {@link TextTable} as a number, read in the table's convention.
|
|
411
|
+
*
|
|
412
|
+
* Not {@link cellNumber}, which reads every comma as a decimal mark — right for a
|
|
413
|
+
* cell typed on its own, wrong for a cell of a file written with decimal points,
|
|
414
|
+
* where `1,234` is a grouped thousand and reading it as 1.234 would be off by a
|
|
415
|
+
* factor of a thousand without a word. Here a comma is a decimal mark only in a
|
|
416
|
+
* table whose convention it is, and a cell with both marks is `NaN` either way: the
|
|
417
|
+
* lexer does not read grouped numbers, it reports them. An empty cell is `NaN` too —
|
|
418
|
+
* a hole in the table, not a zero.
|
|
419
|
+
*/
|
|
420
|
+
export function tableNumber(cell: string, decimalComma: boolean): number {
|
|
421
|
+
const trimmed = cell.trim();
|
|
422
|
+
if (trimmed === "") return Number.NaN;
|
|
423
|
+
return Number(decimalComma ? point(trimmed) : trimmed);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const SEPARATOR_TRUST: readonly TableSeparator[] = ["\t", ";", ",", " "];
|
|
427
|
+
|
|
428
|
+
/** {@link contentLines} for a table of text: spaces come off the ends, but a TAB
|
|
429
|
+
* stays — in a tab-separated export a leading tab is an empty first cell, and
|
|
430
|
+
* trimming it would slide the whole row one column to the left. */
|
|
431
|
+
function textLines(text: string): { line: string; number: number }[] {
|
|
432
|
+
return text
|
|
433
|
+
.split(/\r?\n/)
|
|
434
|
+
.map((line, index) => ({ line: line.replace(/^ +| +$/g, ""), number: index + 1 }))
|
|
435
|
+
.filter(({ line }) => line.trim() !== "" && !line.trimStart().startsWith("#"));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The separator most of the first lines are written with — a title block above the
|
|
439
|
+
* table loses the vote to the table under it. A tie goes to the more trusted one. */
|
|
440
|
+
function votedSeparator(lines: { line: string }[]): TableSeparator {
|
|
441
|
+
const votes = new Map<TableSeparator, number>();
|
|
442
|
+
for (const { line } of lines.slice(0, 50)) {
|
|
443
|
+
const found = separatorOf(withoutQuoted(line));
|
|
444
|
+
const named: TableSeparator = typeof found === "string" ? (found as TableSeparator) : " ";
|
|
445
|
+
votes.set(named, (votes.get(named) ?? 0) + 1);
|
|
446
|
+
}
|
|
447
|
+
let best: TableSeparator = SEPARATOR_TRUST[0];
|
|
448
|
+
let bestCount = -1;
|
|
449
|
+
for (const candidate of SEPARATOR_TRUST) {
|
|
450
|
+
const count = votes.get(candidate) ?? 0;
|
|
451
|
+
if (count > bestCount) {
|
|
452
|
+
best = candidate;
|
|
453
|
+
bestCount = count;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return best;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** A line with its quoted parts emptied, so a separator inside quotes does not vote. */
|
|
460
|
+
const withoutQuoted = (line: string) => line.replace(/"(?:[^"]|"")*"/g, '""');
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* One line into cells: trimmed, quotes taken off, empty cells kept in their place.
|
|
464
|
+
* A run of whitespace is one separator and makes no empty cells.
|
|
465
|
+
*/
|
|
466
|
+
function cellsOf(line: string, separator: TableSeparator): string[] {
|
|
467
|
+
if (separator === " ") return line.split(/\s+/).filter((field) => field !== "").map(unquote);
|
|
468
|
+
const cells: string[] = [];
|
|
469
|
+
let cell = "";
|
|
470
|
+
let quoted = false;
|
|
471
|
+
let started = false;
|
|
472
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
473
|
+
const char = line[index];
|
|
474
|
+
if (quoted) {
|
|
475
|
+
if (char === '"' && line[index + 1] === '"') {
|
|
476
|
+
cell += '"';
|
|
477
|
+
index += 1;
|
|
478
|
+
} else if (char === '"') {
|
|
479
|
+
quoted = false;
|
|
480
|
+
} else {
|
|
481
|
+
cell += char;
|
|
482
|
+
}
|
|
483
|
+
} else if (char === separator) {
|
|
484
|
+
cells.push(cell.trim());
|
|
485
|
+
cell = "";
|
|
486
|
+
started = false;
|
|
487
|
+
} else if (char === '"' && !started) {
|
|
488
|
+
quoted = true;
|
|
489
|
+
started = true;
|
|
490
|
+
cell = "";
|
|
491
|
+
} else {
|
|
492
|
+
if (char.trim() !== "") started = true;
|
|
493
|
+
cell += char;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
cells.push(cell.trim());
|
|
497
|
+
return cells;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** A whitespace-separated field with its quotes off. */
|
|
501
|
+
function unquote(field: string): string {
|
|
502
|
+
return field.length >= 2 && field.startsWith('"') && field.endsWith('"') ? field.slice(1, -1).replace(/""/g, '"') : field;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** The column count most lines share; a tie goes to the wider table. */
|
|
506
|
+
function commonWidth(counts: number[]): number {
|
|
507
|
+
const tally = new Map<number, number>();
|
|
508
|
+
for (const count of counts) tally.set(count, (tally.get(count) ?? 0) + 1);
|
|
509
|
+
let width = 0;
|
|
510
|
+
let best = 0;
|
|
511
|
+
for (const [count, times] of tally) {
|
|
512
|
+
if (times > best || (times === best && count > width)) {
|
|
513
|
+
width = count;
|
|
514
|
+
best = times;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return width;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** A cell with a letter in it — a name, not a value. */
|
|
521
|
+
const isWord = (cell: string) => /\p{L}/u.test(cell);
|
|
522
|
+
/** A cell made of digits and the marks numbers, dates and times are written with. */
|
|
523
|
+
const isValue = (cell: string) => /\d/.test(cell) && !isWord(cell);
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Whether a first line names the columns under it. A word over a column of values
|
|
527
|
+
* says so; with no rows to compare against, a line of words alone does.
|
|
528
|
+
*/
|
|
529
|
+
function looksLikeHeader(first: string[], below: string[][]): boolean {
|
|
530
|
+
if (!below.length) return first.some((cell) => cell !== "") && first.every((cell) => cell === "" || isWord(cell));
|
|
531
|
+
return first.some((cell, column) => {
|
|
532
|
+
if (!isWord(cell)) return false;
|
|
533
|
+
const values = below.map((row) => row[column]).filter((value) => value !== "");
|
|
534
|
+
return values.length > 0 && values.every(isValue);
|
|
535
|
+
});
|
|
536
|
+
}
|