@eifi1/ui-kit 0.22.0 → 0.24.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 +8 -7
- package/dist/components/amount-input.d.ts +34 -7
- package/dist/components/amount-input.js +14 -10
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +26 -0
- package/dist/components/autocomplete.js +164 -137
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +8 -7
- package/dist/components/calculator.d.ts +8 -7
- package/dist/components/column-mapper.d.ts +235 -0
- package/dist/components/column-mapper.js +411 -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 +113 -3
- package/dist/components/combobox.js +456 -313
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +8 -7
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +5 -4
- package/dist/components/country-select.d.ts +50 -7
- package/dist/components/country-select.js +65 -9
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +8 -7
- package/dist/components/danger-confirm.js +64 -7
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/date-picker.d.ts +41 -0
- package/dist/components/date-picker.js +269 -8
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +48 -2
- package/dist/components/entity-combobox.js +159 -114
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +8 -7
- package/dist/components/field-parts.d.ts +116 -6
- package/dist/components/field-parts.js +84 -0
- package/dist/components/field-parts.js.map +1 -1
- 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 +8 -7
- package/dist/components/file-dropzone.d.ts +8 -7
- package/dist/components/form-actions.d.ts +8 -7
- package/dist/components/form-actions.js +23 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +8 -7
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +12 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/language-select.d.ts +8 -7
- package/dist/components/money-field.d.ts +11 -7
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +2 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +31 -2
- package/dist/components/multi-entity-combobox.js +152 -103
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +8 -7
- package/dist/components/number-input.d.ts +8 -7
- package/dist/components/numpad-sheet.d.ts +8 -7
- package/dist/components/phone-input.d.ts +8 -7
- 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 +8 -7
- package/dist/components/settings-fields.d.ts +8 -7
- package/dist/components/share-card.d.ts +8 -7
- package/dist/components/swatch-picker.d.ts +17 -1
- package/dist/components/swatch-picker.js +10 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.d.ts +8 -7
- package/dist/components/tile-radio.d.ts +20 -0
- package/dist/components/tile-radio.js +25 -8
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +8 -7
- package/dist/components/toggle-group.js +2 -2
- 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 +5 -4
- package/dist/components/ui.js +60 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +66 -1
- package/dist/feedback/feedback-attachment.js +185 -65
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +66 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +67 -252
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +66 -287
- package/dist/feedback/feedback-thread.js +16 -3
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
- package/dist/feedback.d.ts +65 -3
- package/dist/hooks/use-file-drop.d.ts +8 -7
- package/dist/i18n/defaults.d.ts +8 -7
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +8 -7
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +8 -7
- package/dist/i18n/locales/de-CH.d.ts +8 -7
- package/dist/i18n/locales/en.d.ts +8 -7
- package/dist/i18n/locales/en.js +7 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +8 -7
- package/dist/i18n/locales/es.js +37 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +8 -7
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +8 -7
- package/dist/i18n/locales/hu.js +38 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +8 -7
- package/dist/i18n/locales/it.js +37 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +8 -7
- package/dist/i18n/locales/zh.js +34 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +8 -7
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +16 -0
- 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/strip-fade.d.ts +2 -2
- package/dist/lib/strip-fade.js.map +1 -1
- 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 +142 -13
- package/dist/rhf/fields.js +185 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +8 -7
- package/dist/rhf.d.ts +9 -8
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +8 -7
- package/dist/shell/top-bar-brand.d.ts +8 -7
- 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 +8 -7
- package/dist/table-text.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +38 -9
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +8 -7
- package/package.json +1 -1
- package/src/components/amount-input.tsx +50 -14
- package/src/components/autocomplete.tsx +188 -116
- package/src/components/column-mapper.tsx +768 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +655 -335
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +135 -22
- package/src/components/danger-confirm.tsx +274 -24
- package/src/components/date-picker.tsx +423 -10
- package/src/components/entity-combobox.tsx +243 -129
- package/src/components/field-parts.tsx +224 -5
- package/src/components/field-strip.tsx +149 -0
- package/src/components/form-actions.tsx +52 -5
- package/src/components/icon-picker.tsx +23 -4
- package/src/components/money-field.tsx +3 -0
- package/src/components/month-picker.tsx +2 -1
- package/src/components/multi-entity-combobox.tsx +221 -121
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/swatch-picker.tsx +29 -5
- package/src/components/tile-radio.tsx +74 -13
- package/src/components/toggle-group.tsx +2 -2
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +114 -33
- package/src/feedback/feedback-attachment.tsx +372 -72
- package/src/feedback/feedback-dialog.tsx +7 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +112 -9
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +42 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/en.ts +27 -5
- package/src/i18n/locales/es.ts +41 -0
- package/src/i18n/locales/fr.ts +42 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +41 -0
- package/src/i18n/locales/zh.ts +32 -0
- package/src/i18n/review.ts +19 -0
- package/src/index.ts +18 -0
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/strip-fade.ts +2 -2
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +421 -6
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/wizard/stepper-nav.tsx +35 -1
- package/dist/components/field-anatomy.d.ts +0 -95
- package/dist/components/field-anatomy.js +0 -84
- package/dist/components/field-anatomy.js.map +0 -1
- package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
- package/src/components/field-anatomy.tsx +0 -190
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/column-mapping.ts"],"sourcesContent":["/**\n * Which column of a table plays which part — the headless half of {@link ColumnMapper}.\n *\n * Two apps built this by hand, the same way. Kurvenschmiede's columns input\n * (`features/control/columns-input.tsx`) reads a recorder's export and asks which\n * column is the time, the setpoint and the actual; keksdose's bank-file import\n * (`import-map-step.tsx`, `use-file-import.ts`) shows a bank's CSV and asks which\n * column is the booking date, the amount, the payee. Both then have the same four\n * rules, and keksdose wrote them down after getting each one wrong once:\n *\n * * **A role sits on one column at most, and a column carries one role.** Picking a\n * role for a column MOVES it there from wherever it was ({@link assignColumnRole});\n * the column it left falls back to \"ignore\" in the same step. There is no error\n * state, because there is nothing wrong to report.\n * * **What is still missing is named**, not left to a disabled button\n * ({@link missingRoles}). A required role can be one of a group: a bank export that\n * splits debit and credit has no amount column and needs none, so `required` may\n * name a group, satisfied by any one of its members (keksdose's \"money\").\n * * **A mapping is indexes into ONE table.** When the column count changes, the\n * indexes mean nothing any more and are guessed afresh; when it does not (the\n * header line toggled), they still point where they did — keksdose's `keepRoles`.\n * * **The rows a role cannot read are reported by line**, not dropped in silence\n * ({@link readMappedTable}): a footer under a recording, a total under a statement.\n *\n * Pure functions over plain data, so a two-step wizard can hold the mapping in its own\n * state between steps, and a table the server sniffed (keksdose's) can be mapped with\n * the same rules as one parsed here.\n */\nimport { tableNumber } from \"./table-text\";\nimport type { TableLine, TextTable } from \"./table-text\";\n\n/** One part a column can play, defined by the caller. */\nexport interface ColumnRole<R extends string = string> {\n /** The key the mapping is stored under. */\n value: R;\n /** What the role select shows — translated by the caller. */\n label: string;\n /**\n * `true`: the table cannot be used until a column plays this role. A string names a\n * GROUP: the table needs any one of the roles that share it — keksdose's amount,\n * debit and credit, any of which carries the money.\n */\n required?: boolean | string;\n /**\n * Every cell this role reads must be a number in the table's decimal convention\n * ({@link tableNumber}). A row where one is not — a footer, a units line, a total\n * with a grouping mark — is moved to `unread` with its line number instead of being\n * handed over. Kurvenschmiede's time, setpoint and actual. The cells stay strings:\n * converting is the caller's, with `tableNumber(cell, result.decimalComma)`.\n */\n numeric?: boolean;\n}\n\n/** Role → column index. A role that is absent or `null` sits on no column — so a\n * mapping with nullable fields (keksdose's `CsvMapping`) is one as it stands. */\nexport type ColumnMapping<R extends string = string> = Partial<Record<R, number | null>>;\n\n/** What the table is, read through a mapping. */\nexport interface ColumnMapperResult<R extends string = string> extends TextTable {\n mapping: ColumnMapping<R>;\n /** What the table still needs before it can be used: one entry per gap, each the\n * roles any of which would fill it (one role, or a required group). */\n missing: R[][];\n /** Every required role placed, and at least one row read. */\n complete: boolean;\n}\n\nconst placedAt = <R extends string>(mapping: ColumnMapping<R>, role: R, width?: number): number | null => {\n const column = mapping[role];\n if (typeof column !== \"number\") return null;\n return width === undefined || column < width ? column : null;\n};\n\n/** The role on this column, or `null` for an ignored one. */\nexport function roleOfColumn<R extends string>(\n roles: readonly ColumnRole<R>[],\n mapping: ColumnMapping<R>,\n column: number,\n): R | null {\n return roles.find((role) => mapping[role.value] === column)?.value ?? null;\n}\n\n/**\n * Put `role` on `column` — or, with `null`, take the column's role off it.\n *\n * Whatever this column played before stops, and the role moves here from any other\n * column, in one new mapping (keksdose's `assignColumn`: \"both halves are patches to\n * the same object\"). Other fields of the mapping are kept as they are.\n */\nexport function assignColumnRole<R extends string, M extends ColumnMapping<R>>(\n roles: readonly ColumnRole<R>[],\n mapping: M,\n column: number,\n role: R | null,\n): M {\n const next = { ...mapping } as Record<string, unknown>;\n for (const { value } of roles) {\n if (mapping[value] === column) next[value] = null;\n }\n if (role !== null) next[role] = column;\n return next as M;\n}\n\n/**\n * The gaps: each required role with no column, and each required group none of whose\n * roles has one. In the order of `roles`. A column index past `width` is no column.\n */\nexport function missingRoles<R extends string>(\n roles: readonly ColumnRole<R>[],\n mapping: ColumnMapping<R>,\n width?: number,\n): R[][] {\n const gaps: R[][] = [];\n const groups = new Set<string>();\n for (const role of roles) {\n if (role.required === true) {\n if (placedAt(mapping, role.value, width) === null) gaps.push([role.value]);\n } else if (typeof role.required === \"string\" && !groups.has(role.required)) {\n groups.add(role.required);\n const members = roles.filter((other) => other.required === role.required);\n if (members.every((member) => placedAt(mapping, member.value, width) === null)) {\n gaps.push(members.map((member) => member.value));\n }\n }\n }\n return gaps;\n}\n\nconst normalised = (text: string) => text.trim().toLowerCase();\n\n/**\n * A first mapping for a table nobody has mapped yet.\n *\n * A column whose header IS a role's label or key gets that role. The required roles\n * left over then take the free columns in order — Kurvenschmiede's \"time, setpoint,\n * actual are the first three columns\", which is how a recorder writes them. Optional\n * roles are not guessed by position: in a bank\n * export of twelve columns, \"the fourth role is the fourth column\" is a wrong answer\n * presented as a suggestion, and an \"ignore\" is the honest one.\n */\nexport function guessMapping<R extends string>(\n roles: readonly ColumnRole<R>[],\n header: readonly string[] | null,\n width: number,\n): ColumnMapping<R> {\n const mapping: ColumnMapping<R> = {};\n const taken = new Set<number>();\n if (header) {\n for (const role of roles) {\n const names = [normalised(role.label), normalised(role.value)];\n const column = header.findIndex((name, index) => !taken.has(index) && names.includes(normalised(name)));\n if (column >= 0) {\n mapping[role.value] = column;\n taken.add(column);\n }\n }\n }\n const free = Array.from({ length: width }, (_, index) => index).filter((index) => !taken.has(index));\n for (const role of roles) {\n if (role.required !== true || typeof mapping[role.value] === \"number\") continue;\n const column = free.shift();\n if (column === undefined) break;\n mapping[role.value] = column;\n }\n return mapping;\n}\n\n/**\n * The table read through the mapping: the rows every `numeric` role can read, the\n * others moved to `unread` (in line order, with the parse's own), what is missing,\n * and whether the table can be used. Column indexes past the table's width count as\n * unplaced.\n */\nexport function readMappedTable<R extends string>(\n table: TextTable,\n roles: readonly ColumnRole<R>[],\n mapping: ColumnMapping<R>,\n): ColumnMapperResult<R> {\n const numeric = roles\n .filter((role) => role.numeric)\n .map((role) => placedAt(mapping, role.value, table.width))\n .filter((column): column is number => column !== null);\n const rows: string[][] = [];\n const lines: TableLine[] = [];\n const refused: TableLine[] = [];\n table.rows.forEach((row, index) => {\n if (numeric.every((column) => Number.isFinite(tableNumber(row[column] ?? \"\", table.decimalComma)))) {\n rows.push(row);\n lines.push(table.lines[index]);\n } else {\n refused.push(table.lines[index]);\n }\n });\n const unread = refused.length ? [...table.unread, ...refused].sort((a, b) => a.line - b.line) : table.unread;\n const missing = missingRoles(roles, mapping, table.width);\n return {\n ...table,\n rows,\n lines,\n unread,\n mapping,\n missing,\n complete: missing.length === 0 && rows.length > 0,\n };\n}\n\n/**\n * A dropped or chosen file's text — UTF-8 when it is UTF-8, else Windows-1252.\n *\n * A German bank or bench export is very often written in Windows-1252, and reading it\n * as UTF-8 turns every \"ä\" in a column name into \"�\" — no error, just a header nobody\n * recognises. So the bytes are tried as strict UTF-8 first and read as Windows-1252\n * when that fails (keksdose's server sniffs the encoding for the same reason; a file\n * read in the browser had nothing doing it). Falls back to `FileReader` where a `File`\n * has no `arrayBuffer` (Kurvenschmiede's `readText`).\n */\nexport async function readTextFile(file: Blob): Promise<string> {\n const bytes = await bytesOf(file);\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n return new TextDecoder(\"windows-1252\").decode(bytes);\n }\n}\n\nfunction bytesOf(file: Blob): Promise<ArrayBuffer> {\n if (typeof file.arrayBuffer === \"function\") return file.arrayBuffer();\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as ArrayBuffer);\n reader.onerror = () => reject(reader.error ?? new Error(\"read failed\"));\n reader.readAsArrayBuffer(file);\n });\n}\n"],"mappings":"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","names":[]}
|
package/dist/lib/strip-fade.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { RefObject } from 'react';
|
|
|
2
2
|
|
|
3
3
|
declare const TAB_FADE_PX = 24;
|
|
4
4
|
/**
|
|
5
|
-
* Shared by Tabs' strip
|
|
6
|
-
* the package's surface.
|
|
5
|
+
* Shared by Tabs' strip, AppShell's scrolling phone sub-nav and (0.24) ColumnRoleTable's
|
|
6
|
+
* preview; internal, not part of the package's surface.
|
|
7
7
|
*
|
|
8
8
|
* Which ends of a sideways-scrolling strip have tabs cut off behind them, as the
|
|
9
9
|
* `mask-image` that fades those ends out — or `undefined` while everything fits.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/strip-fade.ts"],"sourcesContent":["import { useLayoutEffect, useState, type RefObject } from \"react\";\n\n// How far the fade at a scrolled strip's cut edge reaches, and how much room a tab\n// brought into view keeps from that edge so the fade never lies on top of it.\nexport const TAB_FADE_PX = 24;\n\n/**\n * Shared by Tabs' strip
|
|
1
|
+
{"version":3,"sources":["../../src/lib/strip-fade.ts"],"sourcesContent":["import { useLayoutEffect, useState, type RefObject } from \"react\";\n\n// How far the fade at a scrolled strip's cut edge reaches, and how much room a tab\n// brought into view keeps from that edge so the fade never lies on top of it.\nexport const TAB_FADE_PX = 24;\n\n/**\n * Shared by Tabs' strip, AppShell's scrolling phone sub-nav and (0.24) ColumnRoleTable's\n * preview; internal, not part of the package's surface.\n *\n * Which ends of a sideways-scrolling strip have tabs cut off behind them, as the\n * `mask-image` that fades those ends out — or `undefined` while everything fits.\n *\n * A strip that overflowed used to stop dead at its edge: the last tab sliced through\n * mid-word, and nothing on a phone (whose scrollbars are overlays that only show\n * while you drag) said there was more to reach. A fade is the cue — the letters\n * thin out rather than end — and it is measured, not assumed, so a strip that fits\n * is painted exactly as before. Physical `left`/`right` in the gradient, because\n * `mask-image` has no logical directions; the reading direction is read from the\n * element, and `scrollLeft` is a magnitude here because an RTL strip counts it\n * negative from the start edge.\n */\nexport function useStripFade(\n ref: RefObject<HTMLElement | null>,\n): { mask: string | undefined; overflow: \"start\" | \"end\" | \"both\" | undefined } {\n const [edges, setEdges] = useState({ left: false, right: false, rtl: false });\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n const measure = () => {\n const hidden = el.scrollWidth - el.clientWidth;\n const scrolled = Math.abs(el.scrollLeft);\n const rtl = getComputedStyle(el).direction === \"rtl\";\n const atStart = scrolled > 1;\n const atEnd = hidden - scrolled > 1;\n const left = rtl ? atEnd : atStart;\n const right = rtl ? atStart : atEnd;\n setEdges((prev) =>\n prev.left === left && prev.right === right && prev.rtl === rtl ? prev : { left, right, rtl },\n );\n };\n measure();\n el.addEventListener(\"scroll\", measure, { passive: true });\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(measure);\n observer?.observe(el);\n // The tabs, too: a label or a badge that grows changes the overflow without\n // resizing the strip's own box.\n for (const child of Array.from(el.children)) observer?.observe(child);\n return () => {\n el.removeEventListener(\"scroll\", measure);\n observer?.disconnect();\n };\n });\n if (!edges.left && !edges.right) return { mask: undefined, overflow: undefined };\n const l = edges.left ? `transparent, #000 ${TAB_FADE_PX}px` : \"#000\";\n const r = edges.right ? `#000 calc(100% - ${TAB_FADE_PX}px), transparent` : \"#000\";\n const rtl = edges.rtl;\n const start = rtl ? edges.right : edges.left;\n const end = rtl ? edges.left : edges.right;\n return {\n mask: `linear-gradient(to right, ${l}, ${r})`,\n overflow: start && end ? \"both\" : start ? \"start\" : \"end\",\n };\n}\n\n/**\n * Scroll `item` into view inside `strip` — the strip only (`scrollBy` on it, never\n * `scrollIntoView`, which would scroll the page too) — far enough to clear the fade.\n * A no-op when the strip fits or the item is already clear of both edges.\n */\nexport function scrollIntoStrip(strip: HTMLElement, item: HTMLElement): void {\n if (strip.scrollWidth <= strip.clientWidth) return;\n const s = strip.getBoundingClientRect();\n const t = item.getBoundingClientRect();\n const delta =\n t.left < s.left + TAB_FADE_PX\n ? t.left - s.left - TAB_FADE_PX\n : t.right > s.right - TAB_FADE_PX\n ? t.right - s.right + TAB_FADE_PX\n : 0;\n if (delta !== 0) strip.scrollBy({ left: delta });\n}\n"],"mappings":";AAAA,SAAS,iBAAiB,gBAAgC;AAInD,MAAM,cAAc;AAkBpB,SAAS,aACd,KAC8E;AAC9E,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE,MAAM,OAAO,OAAO,OAAO,KAAK,MAAM,CAAC;AAC5E,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,UAAU,MAAM;AACpB,YAAM,SAAS,GAAG,cAAc,GAAG;AACnC,YAAM,WAAW,KAAK,IAAI,GAAG,UAAU;AACvC,YAAMA,OAAM,iBAAiB,EAAE,EAAE,cAAc;AAC/C,YAAM,UAAU,WAAW;AAC3B,YAAM,QAAQ,SAAS,WAAW;AAClC,YAAM,OAAOA,OAAM,QAAQ;AAC3B,YAAM,QAAQA,OAAM,UAAU;AAC9B;AAAA,QAAS,CAAC,SACR,KAAK,SAAS,QAAQ,KAAK,UAAU,SAAS,KAAK,QAAQA,OAAM,OAAO,EAAE,MAAM,OAAO,KAAAA,KAAI;AAAA,MAC7F;AAAA,IACF;AACA,YAAQ;AACR,OAAG,iBAAiB,UAAU,SAAS,EAAE,SAAS,KAAK,CAAC;AACxD,UAAM,WAAW,OAAO,mBAAmB,cAAc,OAAO,IAAI,eAAe,OAAO;AAC1F,cAAU,QAAQ,EAAE;AAGpB,eAAW,SAAS,MAAM,KAAK,GAAG,QAAQ,EAAG,WAAU,QAAQ,KAAK;AACpE,WAAO,MAAM;AACX,SAAG,oBAAoB,UAAU,OAAO;AACxC,gBAAU,WAAW;AAAA,IACvB;AAAA,EACF,CAAC;AACD,MAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,MAAO,QAAO,EAAE,MAAM,QAAW,UAAU,OAAU;AAC/E,QAAM,IAAI,MAAM,OAAO,qBAAqB,WAAW,OAAO;AAC9D,QAAM,IAAI,MAAM,QAAQ,oBAAoB,WAAW,qBAAqB;AAC5E,QAAM,MAAM,MAAM;AAClB,QAAM,QAAQ,MAAM,MAAM,QAAQ,MAAM;AACxC,QAAM,MAAM,MAAM,MAAM,OAAO,MAAM;AACrC,SAAO;AAAA,IACL,MAAM,6BAA6B,CAAC,KAAK,CAAC;AAAA,IAC1C,UAAU,SAAS,MAAM,SAAS,QAAQ,UAAU;AAAA,EACtD;AACF;AAOO,SAAS,gBAAgB,OAAoB,MAAyB;AAC3E,MAAI,MAAM,eAAe,MAAM,YAAa;AAC5C,QAAM,IAAI,MAAM,sBAAsB;AACtC,QAAM,IAAI,KAAK,sBAAsB;AACrC,QAAM,QACJ,EAAE,OAAO,EAAE,OAAO,cACd,EAAE,OAAO,EAAE,OAAO,cAClB,EAAE,QAAQ,EAAE,QAAQ,cAClB,EAAE,QAAQ,EAAE,QAAQ,cACpB;AACR,MAAI,UAAU,EAAG,OAAM,SAAS,EAAE,MAAM,MAAM,CAAC;AACjD;","names":["rtl"]}
|
package/dist/lib/table-text.d.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
|
/** Where the decision about the comma is taken. See the module docstring: the
|
|
51
56
|
* file door decides once for the whole text, the paste door decides per line. */
|
|
@@ -123,5 +128,108 @@ declare const cellNumber: (cell: string) => number;
|
|
|
123
128
|
/** Whether one cell holds a number yet. An empty cell is not one — it is a hole
|
|
124
129
|
* in the measurement, which is a different thing from a zero. */
|
|
125
130
|
declare const isCellNumber: (cell: string) => boolean;
|
|
131
|
+
/**
|
|
132
|
+
* What separates the columns of a {@link TextTable}, named. `" "` is a run of
|
|
133
|
+
* whitespace — one separator however many spaces or tabs it is made of — as in a
|
|
134
|
+
* printed or MATLAB-dumped table.
|
|
135
|
+
*/
|
|
136
|
+
type TableSeparator = "\t" | ";" | "," | " ";
|
|
137
|
+
/** One line of the text as it was handed over: its 1-based number (blank lines
|
|
138
|
+
* counted, so a reader can count to it in the box they pasted into) and its text,
|
|
139
|
+
* trimmed. */
|
|
140
|
+
interface TableLine {
|
|
141
|
+
line: number;
|
|
142
|
+
text: string;
|
|
143
|
+
}
|
|
144
|
+
interface ParseTextTableOptions {
|
|
145
|
+
/** The separator, decided by the caller — a "separated by" override, a format
|
|
146
|
+
* the app already knows. Default: detected (see {@link parseTextTable}). */
|
|
147
|
+
separator?: TableSeparator;
|
|
148
|
+
/** Whether a comma between two digits is a decimal mark. Default: the
|
|
149
|
+
* `"whole-text"` rule, decided once for the file. */
|
|
150
|
+
decimalComma?: boolean;
|
|
151
|
+
/** Whether the table's first line names its columns. Default: detected — a
|
|
152
|
+
* word over a column of values ("Amount" over `-12,50`) is a header. */
|
|
153
|
+
header?: boolean;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A table read as text: every cell a string, as written.
|
|
157
|
+
*
|
|
158
|
+
* The *file* door's decisions, for a table that is not only numbers. Everything a
|
|
159
|
+
* reader has to know to trust it is in the answer — the separator and the decimal
|
|
160
|
+
* mark that were assumed, and the lines that were not read, with their number and
|
|
161
|
+
* their text — because a guess nobody can see is a guess nobody can correct.
|
|
162
|
+
*/
|
|
163
|
+
interface TextTable {
|
|
164
|
+
/** The first line's cells, when it names the columns; `null` when it is data. */
|
|
165
|
+
header: string[] | null;
|
|
166
|
+
/** Each read row, `width` cells long. An empty cell is `""`, kept in its place. */
|
|
167
|
+
rows: string[][];
|
|
168
|
+
/** Where each row came from — row-aligned with `rows`. */
|
|
169
|
+
lines: TableLine[];
|
|
170
|
+
/** How many columns the table has. */
|
|
171
|
+
width: number;
|
|
172
|
+
separator: TableSeparator;
|
|
173
|
+
/** Whether a comma is this table's decimal mark — the convention its numbers are
|
|
174
|
+
* written in, to read them with {@link tableNumber} and to say back to the reader. */
|
|
175
|
+
decimalComma: boolean;
|
|
176
|
+
/** Lines that are not a row of this table: a title above it, a total under it, a
|
|
177
|
+
* row with a cell too many or too few. */
|
|
178
|
+
unread: TableLine[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A pasted or dropped table, read as text — the lexer's file door for a table whose
|
|
182
|
+
* cells are not all numbers (a bank export: a date, a payee, an amount), and the
|
|
183
|
+
* parse under {@link ColumnMapper}.
|
|
184
|
+
*
|
|
185
|
+
* Moved under the same roof as {@link parseTable} because it has the same three
|
|
186
|
+
* questions — what separates the columns, what the comma is, whether the first
|
|
187
|
+
* line is a header — and answering them a second time somewhere else is how the
|
|
188
|
+
* three readers this module replaced came to disagree. Kurvenschmiede's columns
|
|
189
|
+
* input and keksdose's bank-file import both read a table whose columns the reader
|
|
190
|
+
* then assigns, and both state what was assumed; this is that reading, once.
|
|
191
|
+
*
|
|
192
|
+
* Where it differs from {@link parseTable}, and why:
|
|
193
|
+
*
|
|
194
|
+
* * **Cells stay text.** Which cell is a number is the caller's question (its
|
|
195
|
+
* column roles), so nothing here converts. {@link tableNumber} reads one in the
|
|
196
|
+
* table's own convention.
|
|
197
|
+
* * **An empty cell keeps its place.** A bank row with no memo has `;;` in it, and
|
|
198
|
+
* dropping that field — which the numeric lexer does — would move the amount into
|
|
199
|
+
* the memo's column. A trailing separator that ends most lines (`1;2;3;`, which
|
|
200
|
+
* spreadsheets write) is a line ending, not a column.
|
|
201
|
+
* * **Quoted cells.** `"Example Ltd; branch 2"` is one cell, and `""` inside quotes
|
|
202
|
+
* is a quote. Text has separators in it where numbers never do. A quoted cell that
|
|
203
|
+
* runs over a line break is not joined — the line then has the wrong number of
|
|
204
|
+
* cells and is reported.
|
|
205
|
+
* * **The width is the file's.** The column count most lines share. A line with
|
|
206
|
+
* another count is reported in `unread` rather than padded or cut: a row with a
|
|
207
|
+
* cell too many is a row whose columns have slid, and reading it would put a payee
|
|
208
|
+
* in the amount.
|
|
209
|
+
* * **The separator is voted on**, over the first lines, rather than taken from the
|
|
210
|
+
* second: an export that opens with a title block (a bank's account name and
|
|
211
|
+
* period over the table) would otherwise be read with the title's separator.
|
|
212
|
+
* * **The header is detected by its words.** The numeric lexer's rule — "a line
|
|
213
|
+
* that is not numbers" — cannot work where the rows are text too. A first line is
|
|
214
|
+
* a header when one of its cells is a word and every cell under it, as far as the
|
|
215
|
+
* first rows go, is a value (digits and the marks numbers and dates are written
|
|
216
|
+
* with): "Amount" over `-12,50`, "Date" over `01.02.2026`. A table of words only
|
|
217
|
+
* is read without one; `options.header` says otherwise.
|
|
218
|
+
*
|
|
219
|
+
* Blank lines and `#` banners are skipped, as by the numeric lexer.
|
|
220
|
+
*/
|
|
221
|
+
declare function parseTextTable(text: string, options?: ParseTextTableOptions): TextTable;
|
|
222
|
+
/**
|
|
223
|
+
* One cell of a {@link TextTable} as a number, read in the table's convention.
|
|
224
|
+
*
|
|
225
|
+
* Not {@link cellNumber}, which reads every comma as a decimal mark — right for a
|
|
226
|
+
* cell typed on its own, wrong for a cell of a file written with decimal points,
|
|
227
|
+
* where `1,234` is a grouped thousand and reading it as 1.234 would be off by a
|
|
228
|
+
* factor of a thousand without a word. Here a comma is a decimal mark only in a
|
|
229
|
+
* table whose convention it is, and a cell with both marks is `NaN` either way: the
|
|
230
|
+
* lexer does not read grouped numbers, it reports them. An empty cell is `NaN` too —
|
|
231
|
+
* a hole in the table, not a zero.
|
|
232
|
+
*/
|
|
233
|
+
declare function tableNumber(cell: string, decimalComma: boolean): number;
|
|
126
234
|
|
|
127
|
-
export { type DecimalRule, type ParseTableOptions, type ParsedTable, cellNumber, isCellNumber, parseRows, parseTable, splitRow };
|
|
235
|
+
export { type DecimalRule, type ParseTableOptions, type ParseTextTableOptions, type ParsedTable, type TableLine, type TableSeparator, type TextTable, cellNumber, isCellNumber, parseRows, parseTable, parseTextTable, splitRow, tableNumber };
|
package/dist/lib/table-text.js
CHANGED
|
@@ -72,11 +72,132 @@ function fieldsOf(line, separator) {
|
|
|
72
72
|
return line.split(separator).map((field) => field.trim()).filter((field) => field !== "");
|
|
73
73
|
}
|
|
74
74
|
const point = (field) => field.replace(",", ".");
|
|
75
|
+
function parseTextTable(text, options = {}) {
|
|
76
|
+
const content = textLines(text);
|
|
77
|
+
if (!content.length) {
|
|
78
|
+
return { header: null, rows: [], lines: [], width: 0, separator: options.separator ?? ",", decimalComma: false, unread: [] };
|
|
79
|
+
}
|
|
80
|
+
const separator = options.separator ?? votedSeparator(content);
|
|
81
|
+
const decimalComma = options.decimalComma ?? (separator !== "," && DECIMAL_COMMA.test(content.map((entry) => entry.line).join("\n")));
|
|
82
|
+
const trailing = separator !== " " && content.filter(({ line }) => line.endsWith(separator)).length * 2 > content.length;
|
|
83
|
+
const split = content.map(({ line, number }) => {
|
|
84
|
+
const cells = cellsOf(line, separator);
|
|
85
|
+
if (trailing && line.endsWith(separator) && cells[cells.length - 1] === "") cells.pop();
|
|
86
|
+
return { cells, entry: { line: number, text: line } };
|
|
87
|
+
});
|
|
88
|
+
const width = commonWidth(split.map(({ cells }) => cells.length));
|
|
89
|
+
const first = split.findIndex(({ cells }) => cells.length === width);
|
|
90
|
+
const unread = split.slice(0, first).map(({ entry }) => entry);
|
|
91
|
+
const body = split.slice(first);
|
|
92
|
+
const fitting = body.filter(({ cells }) => cells.length === width);
|
|
93
|
+
const header = options.header ?? looksLikeHeader(fitting[0].cells, fitting.slice(1, 21).map(({ cells }) => cells));
|
|
94
|
+
const rows = [];
|
|
95
|
+
const lines = [];
|
|
96
|
+
body.forEach(({ cells, entry }, index) => {
|
|
97
|
+
if (index === 0 && header) return;
|
|
98
|
+
if (cells.length !== width) {
|
|
99
|
+
unread.push(entry);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
rows.push(cells);
|
|
103
|
+
lines.push(entry);
|
|
104
|
+
});
|
|
105
|
+
return { header: header ? body[0].cells : null, rows, lines, width, separator, decimalComma, unread };
|
|
106
|
+
}
|
|
107
|
+
function tableNumber(cell, decimalComma) {
|
|
108
|
+
const trimmed = cell.trim();
|
|
109
|
+
if (trimmed === "") return Number.NaN;
|
|
110
|
+
return Number(decimalComma ? point(trimmed) : trimmed);
|
|
111
|
+
}
|
|
112
|
+
const SEPARATOR_TRUST = [" ", ";", ",", " "];
|
|
113
|
+
function textLines(text) {
|
|
114
|
+
return text.split(/\r?\n/).map((line, index) => ({ line: line.replace(/^ +| +$/g, ""), number: index + 1 })).filter(({ line }) => line.trim() !== "" && !line.trimStart().startsWith("#"));
|
|
115
|
+
}
|
|
116
|
+
function votedSeparator(lines) {
|
|
117
|
+
const votes = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const { line } of lines.slice(0, 50)) {
|
|
119
|
+
const found = separatorOf(withoutQuoted(line));
|
|
120
|
+
const named = typeof found === "string" ? found : " ";
|
|
121
|
+
votes.set(named, (votes.get(named) ?? 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
let best = SEPARATOR_TRUST[0];
|
|
124
|
+
let bestCount = -1;
|
|
125
|
+
for (const candidate of SEPARATOR_TRUST) {
|
|
126
|
+
const count = votes.get(candidate) ?? 0;
|
|
127
|
+
if (count > bestCount) {
|
|
128
|
+
best = candidate;
|
|
129
|
+
bestCount = count;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return best;
|
|
133
|
+
}
|
|
134
|
+
const withoutQuoted = (line) => line.replace(/"(?:[^"]|"")*"/g, '""');
|
|
135
|
+
function cellsOf(line, separator) {
|
|
136
|
+
if (separator === " ") return line.split(/\s+/).filter((field) => field !== "").map(unquote);
|
|
137
|
+
const cells = [];
|
|
138
|
+
let cell = "";
|
|
139
|
+
let quoted = false;
|
|
140
|
+
let started = false;
|
|
141
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
142
|
+
const char = line[index];
|
|
143
|
+
if (quoted) {
|
|
144
|
+
if (char === '"' && line[index + 1] === '"') {
|
|
145
|
+
cell += '"';
|
|
146
|
+
index += 1;
|
|
147
|
+
} else if (char === '"') {
|
|
148
|
+
quoted = false;
|
|
149
|
+
} else {
|
|
150
|
+
cell += char;
|
|
151
|
+
}
|
|
152
|
+
} else if (char === separator) {
|
|
153
|
+
cells.push(cell.trim());
|
|
154
|
+
cell = "";
|
|
155
|
+
started = false;
|
|
156
|
+
} else if (char === '"' && !started) {
|
|
157
|
+
quoted = true;
|
|
158
|
+
started = true;
|
|
159
|
+
cell = "";
|
|
160
|
+
} else {
|
|
161
|
+
if (char.trim() !== "") started = true;
|
|
162
|
+
cell += char;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
cells.push(cell.trim());
|
|
166
|
+
return cells;
|
|
167
|
+
}
|
|
168
|
+
function unquote(field) {
|
|
169
|
+
return field.length >= 2 && field.startsWith('"') && field.endsWith('"') ? field.slice(1, -1).replace(/""/g, '"') : field;
|
|
170
|
+
}
|
|
171
|
+
function commonWidth(counts) {
|
|
172
|
+
const tally = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const count of counts) tally.set(count, (tally.get(count) ?? 0) + 1);
|
|
174
|
+
let width = 0;
|
|
175
|
+
let best = 0;
|
|
176
|
+
for (const [count, times] of tally) {
|
|
177
|
+
if (times > best || times === best && count > width) {
|
|
178
|
+
width = count;
|
|
179
|
+
best = times;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return width;
|
|
183
|
+
}
|
|
184
|
+
const isWord = (cell) => /\p{L}/u.test(cell);
|
|
185
|
+
const isValue = (cell) => /\d/.test(cell) && !isWord(cell);
|
|
186
|
+
function looksLikeHeader(first, below) {
|
|
187
|
+
if (!below.length) return first.some((cell) => cell !== "") && first.every((cell) => cell === "" || isWord(cell));
|
|
188
|
+
return first.some((cell, column) => {
|
|
189
|
+
if (!isWord(cell)) return false;
|
|
190
|
+
const values = below.map((row) => row[column]).filter((value) => value !== "");
|
|
191
|
+
return values.length > 0 && values.every(isValue);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
75
194
|
export {
|
|
76
195
|
cellNumber,
|
|
77
196
|
isCellNumber,
|
|
78
197
|
parseRows,
|
|
79
198
|
parseTable,
|
|
80
|
-
|
|
199
|
+
parseTextTable,
|
|
200
|
+
splitRow,
|
|
201
|
+
tableNumber
|
|
81
202
|
};
|
|
82
203
|
//# sourceMappingURL=table-text.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/table-text.ts"],"sourcesContent":["/**\n * One lexer for \"paste the table out of a spreadsheet\", and the three rules for the\n * comma. `@eifi1/ui-kit/table-text` — pure functions over strings, no React.\n *\n * Moved here from Lenkbank (`shared/lib/parse-table.ts`), where three screens took a\n * table off somebody's clipboard — a measured-table grid, the text view beside it, and\n * a page that reads a recorder's setpoint/actual files — and each had grown its own\n * reader. They disagreed, and not in the harmless way: a whitespace-separated German\n * export was read as six columns of nonsense by two of them and as nothing at all by\n * the third, with no message either way. The lexing is one problem (lines, a\n * separator, fields, a header, numbers) and it is solved once here, so the next app\n * that imports a CSV does not write the comma rules wrong a second time.\n *\n * **What is genuinely different between callers is where the decision about the\n * comma is taken**, and that is a parameter rather than a constant. A comma is the\n * decimal mark in every German export and the column separator in every English CSV,\n * and no single rule serves both without regressing one of them. So the rules are\n * named instead of one being picked:\n *\n * * **`\"whole-text\"`** — the *file* door. A recorder writes one file in one\n * convention, so the separator and the decimal mark are decided once, from the\n * whole text, and stated back to the reader (`decimalComma`) where they can see\n * what was assumed. Deciding once is what lets one damaged line be *counted*\n * rather than re-guessed into a different convention than the lines around it.\n * * **`\"per-line\"`** — the *paste* door. What arrives on a clipboard is a few rows\n * out of a spreadsheet, sometimes one row, sometimes a line somebody typed. There\n * is no file to be consistent with, and a line must not change meaning because of\n * another line somewhere else in the box — so each line is read on its own\n * evidence.\n * * **unconditional** — one cell (`cellNumber` / `isCellNumber`). A single cell has\n * no columns in it, so a comma in one cannot be separating anything and is always\n * a decimal mark. That is not a table rule at all, which is why it is a pair of\n * one-liners beside the others rather than an option on them.\n *\n * What this deliberately does not do:\n *\n * * **Thousands separators.** `1.234,5` and `1,234.5` are both a number with two\n * marks in it, and telling the grouping mark from the decimal mark needs a rule\n * about digit runs that would then have to be right for a three-digit integer\n * too. A field carrying one is reported as a line that could not be read, with\n * its line number, which is something a reader can act on. Guessing is not.\n * * **Empty cells.** An empty field is dropped rather than held open as a hole, so a\n * row with a dropped channel in the middle of it shifts left. Keeping the hole\n * would mean reading `1;2;3;` — a trailing separator, which every spreadsheet\n * writes — as a four-column row. It is a known gap, not an oversight.\n * * **Quoted fields.** No `\"a;b\"` escaping. These are numbers; the only text in them\n * is the header line, and a header field with a separator in it costs a column\n * name, not a measurement.\n */\n\n/** A digit, a comma, a digit — a comma that can only be a decimal mark or a\n * grouping mark, never a separator with a number on each side of it. `1, 2`\n * deliberately does not match: a comma followed by a space is how a human\n * writes a list, and reading it as a decimal mark would turn a hand-typed\n * `1, 2, 3` into one number. */\nconst DECIMAL_COMMA = /\\d,\\d/;\n\n/** What separates the columns of one line. A regular expression only for the\n * whitespace case, because a run of spaces is one separator. */\ntype Separator = string | RegExp;\n\n/** Where the decision about the comma is taken. See the module docstring: the\n * file door decides once for the whole text, the paste door decides per line. */\nexport type DecimalRule = \"per-line\" | \"whole-text\";\n\nexport interface ParseTableOptions {\n decimal: DecimalRule;\n /**\n * How many columns the caller already knows the table has. A grid with fixed\n * columns knows its width before a number arrives: anything past it is sliced\n * off — an export with a trailing timestamp column is not an error worth\n * refusing — and a line with fewer is reported. Omitted where the width is the\n * file's own answer.\n */\n columns?: number;\n /**\n * How many leading lines that are not rows of numbers may be a header.\n * Default 1, the name row. {@link parseRows} passes 2, because a bench export\n * writes the units on a second line under the names.\n */\n headerLines?: number;\n}\n\nexport interface ParsedTable {\n rows: number[][];\n /** The first header line's fields — the column *names*. A units line under it\n * is absorbed and not returned: what a column is measured in is the caller's to\n * say (its column definitions), and a second list of strings here would be a\n * second answer to that. */\n header: string[] | null;\n /** Whether a comma was read as a decimal mark. Under `\"per-line\"` this is true\n * if any line was read that way, and it exists for the reader: a guess\n * nobody can see is a guess nobody can correct. */\n decimalComma: boolean;\n /** Line numbers — 1-based, counted in the text as it was handed over, blank\n * lines included — that held something which is not a row of numbers. A list\n * rather than a count, because one door names the first one and the other\n * counts them. */\n skipped: number[];\n}\n\n/**\n * The columns of one line, as text, with the decimal mark normalised to a point.\n *\n * Text rather than numbers because a grid holds cells as strings: a pasted\n * field that is not a number belongs in the cell it landed in, marked, where the\n * person who pasted it can see which column went wrong. Dropping it here would\n * shift every column after it and lose the evidence.\n */\nexport function splitRow(line: string): string[] {\n const separator = separatorOf(line);\n const decimalComma = separator !== \",\";\n return fieldsOf(line, separator).map((field) => (decimalComma ? point(field) : field));\n}\n\n/**\n * A pasted table, or the lines of it that are not one.\n *\n * The lexer both doors are built on. It reads lines, drops blanks and `#`\n * banners, decides what separates the columns, takes the header off the top and\n * converts what is left.\n */\nexport function parseTable(text: string, options: ParseTableOptions): ParsedTable {\n const headerLimit = options.headerLines ?? 1;\n const lines = contentLines(text);\n if (!lines.length) return { rows: [], header: null, decimalComma: false, skipped: [] };\n const fixed = options.decimal === \"whole-text\" ? wholeTextRule(lines) : null;\n\n let header: string[] | null = null;\n let headerCount = 0;\n const rows: number[][] = [];\n const skipped: number[] = [];\n let sawDecimalComma = false;\n\n for (const { line, number } of lines) {\n const separator = fixed ? fixed.separator : separatorOf(line);\n const decimalComma = fixed ? fixed.decimalComma : separator !== \",\";\n const fields = fieldsOf(line, separator);\n const numbers = fields.map((field) => Number(decimalComma ? point(field) : field));\n const readable = fields.length > 0 && numbers.every(Number.isFinite);\n\n if (!readable) {\n // A header is only a header at the top, and only before any data: a stray\n // word further down is a broken row, and dropping it silently would lose a\n // measurement without saying so.\n if (rows.length === 0 && headerCount < headerLimit) {\n header ??= fields;\n headerCount += 1;\n continue;\n }\n skipped.push(number);\n continue;\n }\n if (options.columns !== undefined && fields.length < options.columns) {\n skipped.push(number);\n continue;\n }\n if (decimalComma && DECIMAL_COMMA.test(line)) sawDecimalComma = true;\n rows.push(options.columns === undefined ? numbers : numbers.slice(0, options.columns));\n }\n\n return {\n rows,\n header,\n decimalComma: fixed ? fixed.decimalComma : sawDecimalComma,\n skipped,\n };\n}\n\n/**\n * The paste door: a fixed number of columns, and the line that stopped it.\n *\n * Read per line, with up to two header lines (names, then units). It answers with a\n * line number rather than with what it managed to read, because half a pasted\n * sweep saved as a measurement is worse than a refused paste — and because a\n * table that comes back **empty** is the one answer a reader cannot act on: it\n * looks exactly like a paste that did not happen. So text that has content and\n * produced no row at all is an error naming its first unreadable line, never\n * `{ rows: [] }`.\n */\nexport function parseRows(text: string, columns: number): { rows: number[][] } | { error: number } {\n const lines = contentLines(text);\n if (!lines.length) return { rows: [] };\n const parsed = parseTable(text, { decimal: \"per-line\", columns, headerLines: 2 });\n if (parsed.skipped.length) return { error: parsed.skipped[0] };\n if (!parsed.rows.length) return { error: lines[0].number };\n return { rows: parsed.rows };\n}\n\n/** One cell as a number. Unconditional: a cell has no columns in it, so a comma\n * in one is a decimal mark and nothing else. */\nexport const cellNumber = (cell: string) => Number(cell.replace(\",\", \".\"));\n\n/** Whether one cell holds a number yet. An empty cell is not one — it is a hole\n * in the measurement, which is a different thing from a zero. */\nexport const isCellNumber = (cell: string) =>\n cell.trim() !== \"\" && !Number.isNaN(cellNumber(cell));\n\n/**\n * The one separator and the one decimal mark a whole file is read with.\n *\n * The separator is taken from the line most likely to be data — the second,\n * where there is one, because the first is usually the names.\n *\n * A comma between two digits is then a decimal mark whenever something else is\n * already separating the columns: there is nothing else it could be. The rule\n * this replaces also demanded that no decimal POINT appear anywhere in the file,\n * which read a semicolon-separated export with one column written by a different\n * tool — an entirely ordinary thing for a bench to produce — as a table with no\n * rows in it, and said nothing.\n */\nfunction wholeTextRule(lines: { line: string }[]): { separator: Separator; decimalComma: boolean } {\n const separator = separatorOf(lines[lines.length > 1 ? 1 : 0].line);\n const body = lines.map((entry) => entry.line).join(\"\\n\");\n return { separator, decimalComma: separator !== \",\" && DECIMAL_COMMA.test(body) };\n}\n\n/** Lines with something on them, numbered as they stand in the original text so\n * a message can point at one. `#` banners are a recorder's, not a measurement. */\nfunction contentLines(text: string): { line: string; number: number }[] {\n return text\n .split(/\\r?\\n/)\n .map((line, index) => ({ line: line.trim(), number: index + 1 }))\n .filter(({ line }) => line !== \"\" && !line.startsWith(\"#\"));\n}\n\n/**\n * What separates this line's columns.\n *\n * In order of trust, not of count: a semicolon-separated line with decimal\n * commas has more commas than semicolons, and a tab-separated one may have both.\n * Whichever of tab and semicolon appears wins outright, because neither is ever\n * anything but a separator.\n *\n * The comma is the one that has to be argued about. A line that has whitespace\n * between its fields *and* a comma between two digits has already said what\n * separates it — so the comma is a decimal mark, and the whitespace separates.\n * That is a German export printed or dumped from MATLAB (`0,5 1,25 2,75`), and\n * reading its commas as separators produced six columns from three: each number\n * split into a pair of small integers, accepted as a row, and saved as a\n * measurement with nothing on screen to say so. A comma with a space after it is\n * not that shape (`DECIMAL_COMMA` does not match `1, 2`), so a hand-typed list\n * still separates on the comma.\n */\nfunction separatorOf(line: string): Separator {\n if (line.includes(\"\\t\")) return \"\\t\";\n if (line.includes(\";\")) return \";\";\n if (line.includes(\",\")) return /\\s/.test(line) && DECIMAL_COMMA.test(line) ? /\\s+/ : \",\";\n return /\\s+/;\n}\n\n/** One line into trimmed, non-empty fields, as they were written. The header is\n * returned out of these, so a column called `Weg,X` keeps its name even where\n * the numbers under it are being read with a decimal comma. */\nfunction fieldsOf(line: string, separator: Separator): string[] {\n return line\n .split(separator)\n .map((field) => field.trim())\n .filter((field) => field !== \"\");\n}\n\n/** The decimal mark this language of `Number` understands. One comma, because a\n * field with two of them is a grouped number and this module does not read\n * those — it reports the line instead. */\nconst point = (field: string) => field.replace(\",\", \".\");\n"],"mappings":";AAuDA,MAAM,gBAAgB;AAsDf,SAAS,SAAS,MAAwB;AAC/C,QAAM,YAAY,YAAY,IAAI;AAClC,QAAM,eAAe,cAAc;AACnC,SAAO,SAAS,MAAM,SAAS,EAAE,IAAI,CAAC,UAAW,eAAe,MAAM,KAAK,IAAI,KAAM;AACvF;AASO,SAAS,WAAW,MAAc,SAAyC;AAChF,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,QAAQ,aAAa,IAAI;AAC/B,MAAI,CAAC,MAAM,OAAQ,QAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,MAAM,cAAc,OAAO,SAAS,CAAC,EAAE;AACrF,QAAM,QAAQ,QAAQ,YAAY,eAAe,cAAc,KAAK,IAAI;AAExE,MAAI,SAA0B;AAC9B,MAAI,cAAc;AAClB,QAAM,OAAmB,CAAC;AAC1B,QAAM,UAAoB,CAAC;AAC3B,MAAI,kBAAkB;AAEtB,aAAW,EAAE,MAAM,OAAO,KAAK,OAAO;AACpC,UAAM,YAAY,QAAQ,MAAM,YAAY,YAAY,IAAI;AAC5D,UAAM,eAAe,QAAQ,MAAM,eAAe,cAAc;AAChE,UAAM,SAAS,SAAS,MAAM,SAAS;AACvC,UAAM,UAAU,OAAO,IAAI,CAAC,UAAU,OAAO,eAAe,MAAM,KAAK,IAAI,KAAK,CAAC;AACjF,UAAM,WAAW,OAAO,SAAS,KAAK,QAAQ,MAAM,OAAO,QAAQ;AAEnE,QAAI,CAAC,UAAU;AAIb,UAAI,KAAK,WAAW,KAAK,cAAc,aAAa;AAClD,mBAAW;AACX,uBAAe;AACf;AAAA,MACF;AACA,cAAQ,KAAK,MAAM;AACnB;AAAA,IACF;AACA,QAAI,QAAQ,YAAY,UAAa,OAAO,SAAS,QAAQ,SAAS;AACpE,cAAQ,KAAK,MAAM;AACnB;AAAA,IACF;AACA,QAAI,gBAAgB,cAAc,KAAK,IAAI,EAAG,mBAAkB;AAChE,SAAK,KAAK,QAAQ,YAAY,SAAY,UAAU,QAAQ,MAAM,GAAG,QAAQ,OAAO,CAAC;AAAA,EACvF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc,QAAQ,MAAM,eAAe;AAAA,IAC3C;AAAA,EACF;AACF;AAaO,SAAS,UAAU,MAAc,SAA2D;AACjG,QAAM,QAAQ,aAAa,IAAI;AAC/B,MAAI,CAAC,MAAM,OAAQ,QAAO,EAAE,MAAM,CAAC,EAAE;AACrC,QAAM,SAAS,WAAW,MAAM,EAAE,SAAS,YAAY,SAAS,aAAa,EAAE,CAAC;AAChF,MAAI,OAAO,QAAQ,OAAQ,QAAO,EAAE,OAAO,OAAO,QAAQ,CAAC,EAAE;AAC7D,MAAI,CAAC,OAAO,KAAK,OAAQ,QAAO,EAAE,OAAO,MAAM,CAAC,EAAE,OAAO;AACzD,SAAO,EAAE,MAAM,OAAO,KAAK;AAC7B;AAIO,MAAM,aAAa,CAAC,SAAiB,OAAO,KAAK,QAAQ,KAAK,GAAG,CAAC;AAIlE,MAAM,eAAe,CAAC,SAC3B,KAAK,KAAK,MAAM,MAAM,CAAC,OAAO,MAAM,WAAW,IAAI,CAAC;AAetD,SAAS,cAAc,OAA4E;AACjG,QAAM,YAAY,YAAY,MAAM,MAAM,SAAS,IAAI,IAAI,CAAC,EAAE,IAAI;AAClE,QAAM,OAAO,MAAM,IAAI,CAAC,UAAU,MAAM,IAAI,EAAE,KAAK,IAAI;AACvD,SAAO,EAAE,WAAW,cAAc,cAAc,OAAO,cAAc,KAAK,IAAI,EAAE;AAClF;AAIA,SAAS,aAAa,MAAkD;AACtE,SAAO,KACJ,MAAM,OAAO,EACb,IAAI,CAAC,MAAM,WAAW,EAAE,MAAM,KAAK,KAAK,GAAG,QAAQ,QAAQ,EAAE,EAAE,EAC/D,OAAO,CAAC,EAAE,KAAK,MAAM,SAAS,MAAM,CAAC,KAAK,WAAW,GAAG,CAAC;AAC9D;AAoBA,SAAS,YAAY,MAAyB;AAC5C,MAAI,KAAK,SAAS,GAAI,EAAG,QAAO;AAChC,MAAI,KAAK,SAAS,GAAG,EAAG,QAAO;AAC/B,MAAI,KAAK,SAAS,GAAG,EAAG,QAAO,KAAK,KAAK,IAAI,KAAK,cAAc,KAAK,IAAI,IAAI,QAAQ;AACrF,SAAO;AACT;AAKA,SAAS,SAAS,MAAc,WAAgC;AAC9D,SAAO,KACJ,MAAM,SAAS,EACf,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,CAAC,UAAU,UAAU,EAAE;AACnC;AAKA,MAAM,QAAQ,CAAC,UAAkB,MAAM,QAAQ,KAAK,GAAG;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/table-text.ts"],"sourcesContent":["/**\n * One lexer for \"paste the table out of a spreadsheet\", and the three rules for the\n * comma. `@eifi1/ui-kit/table-text` — pure functions over strings, no React.\n *\n * Moved here from Lenkbank (`shared/lib/parse-table.ts`), where three screens took a\n * table off somebody's clipboard — a measured-table grid, the text view beside it, and\n * a page that reads a recorder's setpoint/actual files — and each had grown its own\n * reader. They disagreed, and not in the harmless way: a whitespace-separated German\n * export was read as six columns of nonsense by two of them and as nothing at all by\n * the third, with no message either way. The lexing is one problem (lines, a\n * separator, fields, a header, numbers) and it is solved once here, so the next app\n * that imports a CSV does not write the comma rules wrong a second time.\n *\n * **What is genuinely different between callers is where the decision about the\n * comma is taken**, and that is a parameter rather than a constant. A comma is the\n * decimal mark in every German export and the column separator in every English CSV,\n * and no single rule serves both without regressing one of them. So the rules are\n * named instead of one being picked:\n *\n * * **`\"whole-text\"`** — the *file* door. A recorder writes one file in one\n * convention, so the separator and the decimal mark are decided once, from the\n * whole text, and stated back to the reader (`decimalComma`) where they can see\n * what was assumed. Deciding once is what lets one damaged line be *counted*\n * rather than re-guessed into a different convention than the lines around it.\n * * **`\"per-line\"`** — the *paste* door. What arrives on a clipboard is a few rows\n * out of a spreadsheet, sometimes one row, sometimes a line somebody typed. There\n * is no file to be consistent with, and a line must not change meaning because of\n * another line somewhere else in the box — so each line is read on its own\n * evidence.\n * * **unconditional** — one cell (`cellNumber` / `isCellNumber`). A single cell has\n * no columns in it, so a comma in one cannot be separating anything and is always\n * a decimal mark. That is not a table rule at all, which is why it is a pair of\n * one-liners beside the others rather than an option on them.\n *\n * What this deliberately does not do:\n *\n * * **Thousands separators.** `1.234,5` and `1,234.5` are both a number with two\n * marks in it, and telling the grouping mark from the decimal mark needs a rule\n * about digit runs that would then have to be right for a three-digit integer\n * too. A field carrying one is reported as a line that could not be read, with\n * its line number, which is something a reader can act on. Guessing is not.\n * * **Empty cells.** An empty field is dropped rather than held open as a hole, so a\n * row with a dropped channel in the middle of it shifts left. Keeping the hole\n * would mean reading `1;2;3;` — a trailing separator, which every spreadsheet\n * writes — as a four-column row. It is a known gap, not an oversight.\n * * **Quoted fields.** No `\"a;b\"` escaping. These are numbers; the only text in them\n * is the header line, and a header field with a separator in it costs a column\n * name, not a measurement.\n *\n * Both of those are the NUMERIC lexer's limits. A table whose cells are text — a bank\n * export, read for a column mapper — needs both, and {@link parseTextTable} (0.23) is\n * that door: empty cells held in place, quoted cells, and the same separator and comma\n * rules underneath.\n */\n\n/** A digit, a comma, a digit — a comma that can only be a decimal mark or a\n * grouping mark, never a separator with a number on each side of it. `1, 2`\n * deliberately does not match: a comma followed by a space is how a human\n * writes a list, and reading it as a decimal mark would turn a hand-typed\n * `1, 2, 3` into one number. */\nconst DECIMAL_COMMA = /\\d,\\d/;\n\n/** What separates the columns of one line. A regular expression only for the\n * whitespace case, because a run of spaces is one separator. */\ntype Separator = string | RegExp;\n\n/** Where the decision about the comma is taken. See the module docstring: the\n * file door decides once for the whole text, the paste door decides per line. */\nexport type DecimalRule = \"per-line\" | \"whole-text\";\n\nexport interface ParseTableOptions {\n decimal: DecimalRule;\n /**\n * How many columns the caller already knows the table has. A grid with fixed\n * columns knows its width before a number arrives: anything past it is sliced\n * off — an export with a trailing timestamp column is not an error worth\n * refusing — and a line with fewer is reported. Omitted where the width is the\n * file's own answer.\n */\n columns?: number;\n /**\n * How many leading lines that are not rows of numbers may be a header.\n * Default 1, the name row. {@link parseRows} passes 2, because a bench export\n * writes the units on a second line under the names.\n */\n headerLines?: number;\n}\n\nexport interface ParsedTable {\n rows: number[][];\n /** The first header line's fields — the column *names*. A units line under it\n * is absorbed and not returned: what a column is measured in is the caller's to\n * say (its column definitions), and a second list of strings here would be a\n * second answer to that. */\n header: string[] | null;\n /** Whether a comma was read as a decimal mark. Under `\"per-line\"` this is true\n * if any line was read that way, and it exists for the reader: a guess\n * nobody can see is a guess nobody can correct. */\n decimalComma: boolean;\n /** Line numbers — 1-based, counted in the text as it was handed over, blank\n * lines included — that held something which is not a row of numbers. A list\n * rather than a count, because one door names the first one and the other\n * counts them. */\n skipped: number[];\n}\n\n/**\n * The columns of one line, as text, with the decimal mark normalised to a point.\n *\n * Text rather than numbers because a grid holds cells as strings: a pasted\n * field that is not a number belongs in the cell it landed in, marked, where the\n * person who pasted it can see which column went wrong. Dropping it here would\n * shift every column after it and lose the evidence.\n */\nexport function splitRow(line: string): string[] {\n const separator = separatorOf(line);\n const decimalComma = separator !== \",\";\n return fieldsOf(line, separator).map((field) => (decimalComma ? point(field) : field));\n}\n\n/**\n * A pasted table, or the lines of it that are not one.\n *\n * The lexer both doors are built on. It reads lines, drops blanks and `#`\n * banners, decides what separates the columns, takes the header off the top and\n * converts what is left.\n */\nexport function parseTable(text: string, options: ParseTableOptions): ParsedTable {\n const headerLimit = options.headerLines ?? 1;\n const lines = contentLines(text);\n if (!lines.length) return { rows: [], header: null, decimalComma: false, skipped: [] };\n const fixed = options.decimal === \"whole-text\" ? wholeTextRule(lines) : null;\n\n let header: string[] | null = null;\n let headerCount = 0;\n const rows: number[][] = [];\n const skipped: number[] = [];\n let sawDecimalComma = false;\n\n for (const { line, number } of lines) {\n const separator = fixed ? fixed.separator : separatorOf(line);\n const decimalComma = fixed ? fixed.decimalComma : separator !== \",\";\n const fields = fieldsOf(line, separator);\n const numbers = fields.map((field) => Number(decimalComma ? point(field) : field));\n const readable = fields.length > 0 && numbers.every(Number.isFinite);\n\n if (!readable) {\n // A header is only a header at the top, and only before any data: a stray\n // word further down is a broken row, and dropping it silently would lose a\n // measurement without saying so.\n if (rows.length === 0 && headerCount < headerLimit) {\n header ??= fields;\n headerCount += 1;\n continue;\n }\n skipped.push(number);\n continue;\n }\n if (options.columns !== undefined && fields.length < options.columns) {\n skipped.push(number);\n continue;\n }\n if (decimalComma && DECIMAL_COMMA.test(line)) sawDecimalComma = true;\n rows.push(options.columns === undefined ? numbers : numbers.slice(0, options.columns));\n }\n\n return {\n rows,\n header,\n decimalComma: fixed ? fixed.decimalComma : sawDecimalComma,\n skipped,\n };\n}\n\n/**\n * The paste door: a fixed number of columns, and the line that stopped it.\n *\n * Read per line, with up to two header lines (names, then units). It answers with a\n * line number rather than with what it managed to read, because half a pasted\n * sweep saved as a measurement is worse than a refused paste — and because a\n * table that comes back **empty** is the one answer a reader cannot act on: it\n * looks exactly like a paste that did not happen. So text that has content and\n * produced no row at all is an error naming its first unreadable line, never\n * `{ rows: [] }`.\n */\nexport function parseRows(text: string, columns: number): { rows: number[][] } | { error: number } {\n const lines = contentLines(text);\n if (!lines.length) return { rows: [] };\n const parsed = parseTable(text, { decimal: \"per-line\", columns, headerLines: 2 });\n if (parsed.skipped.length) return { error: parsed.skipped[0] };\n if (!parsed.rows.length) return { error: lines[0].number };\n return { rows: parsed.rows };\n}\n\n/** One cell as a number. Unconditional: a cell has no columns in it, so a comma\n * in one is a decimal mark and nothing else. */\nexport const cellNumber = (cell: string) => Number(cell.replace(\",\", \".\"));\n\n/** Whether one cell holds a number yet. An empty cell is not one — it is a hole\n * in the measurement, which is a different thing from a zero. */\nexport const isCellNumber = (cell: string) =>\n cell.trim() !== \"\" && !Number.isNaN(cellNumber(cell));\n\n/**\n * The one separator and the one decimal mark a whole file is read with.\n *\n * The separator is taken from the line most likely to be data — the second,\n * where there is one, because the first is usually the names.\n *\n * A comma between two digits is then a decimal mark whenever something else is\n * already separating the columns: there is nothing else it could be. The rule\n * this replaces also demanded that no decimal POINT appear anywhere in the file,\n * which read a semicolon-separated export with one column written by a different\n * tool — an entirely ordinary thing for a bench to produce — as a table with no\n * rows in it, and said nothing.\n */\nfunction wholeTextRule(lines: { line: string }[]): { separator: Separator; decimalComma: boolean } {\n const separator = separatorOf(lines[lines.length > 1 ? 1 : 0].line);\n const body = lines.map((entry) => entry.line).join(\"\\n\");\n return { separator, decimalComma: separator !== \",\" && DECIMAL_COMMA.test(body) };\n}\n\n/** Lines with something on them, numbered as they stand in the original text so\n * a message can point at one. `#` banners are a recorder's, not a measurement. */\nfunction contentLines(text: string): { line: string; number: number }[] {\n return text\n .split(/\\r?\\n/)\n .map((line, index) => ({ line: line.trim(), number: index + 1 }))\n .filter(({ line }) => line !== \"\" && !line.startsWith(\"#\"));\n}\n\n/**\n * What separates this line's columns.\n *\n * In order of trust, not of count: a semicolon-separated line with decimal\n * commas has more commas than semicolons, and a tab-separated one may have both.\n * Whichever of tab and semicolon appears wins outright, because neither is ever\n * anything but a separator.\n *\n * The comma is the one that has to be argued about. A line that has whitespace\n * between its fields *and* a comma between two digits has already said what\n * separates it — so the comma is a decimal mark, and the whitespace separates.\n * That is a German export printed or dumped from MATLAB (`0,5 1,25 2,75`), and\n * reading its commas as separators produced six columns from three: each number\n * split into a pair of small integers, accepted as a row, and saved as a\n * measurement with nothing on screen to say so. A comma with a space after it is\n * not that shape (`DECIMAL_COMMA` does not match `1, 2`), so a hand-typed list\n * still separates on the comma.\n */\nfunction separatorOf(line: string): Separator {\n if (line.includes(\"\\t\")) return \"\\t\";\n if (line.includes(\";\")) return \";\";\n if (line.includes(\",\")) return /\\s/.test(line) && DECIMAL_COMMA.test(line) ? /\\s+/ : \",\";\n return /\\s+/;\n}\n\n/** One line into trimmed, non-empty fields, as they were written. The header is\n * returned out of these, so a column called `Weg,X` keeps its name even where\n * the numbers under it are being read with a decimal comma. */\nfunction fieldsOf(line: string, separator: Separator): string[] {\n return line\n .split(separator)\n .map((field) => field.trim())\n .filter((field) => field !== \"\");\n}\n\n/** The decimal mark this language of `Number` understands. One comma, because a\n * field with two of them is a grouped number and this module does not read\n * those — it reports the line instead. */\nconst point = (field: string) => field.replace(\",\", \".\");\n\n// ── A table of text ────────────────────────────────────────────────────────────\n\n/**\n * What separates the columns of a {@link TextTable}, named. `\" \"` is a run of\n * whitespace — one separator however many spaces or tabs it is made of — as in a\n * printed or MATLAB-dumped table.\n */\nexport type TableSeparator = \"\\t\" | \";\" | \",\" | \" \";\n\n/** One line of the text as it was handed over: its 1-based number (blank lines\n * counted, so a reader can count to it in the box they pasted into) and its text,\n * trimmed. */\nexport interface TableLine {\n line: number;\n text: string;\n}\n\nexport interface ParseTextTableOptions {\n /** The separator, decided by the caller — a \"separated by\" override, a format\n * the app already knows. Default: detected (see {@link parseTextTable}). */\n separator?: TableSeparator;\n /** Whether a comma between two digits is a decimal mark. Default: the\n * `\"whole-text\"` rule, decided once for the file. */\n decimalComma?: boolean;\n /** Whether the table's first line names its columns. Default: detected — a\n * word over a column of values (\"Amount\" over `-12,50`) is a header. */\n header?: boolean;\n}\n\n/**\n * A table read as text: every cell a string, as written.\n *\n * The *file* door's decisions, for a table that is not only numbers. Everything a\n * reader has to know to trust it is in the answer — the separator and the decimal\n * mark that were assumed, and the lines that were not read, with their number and\n * their text — because a guess nobody can see is a guess nobody can correct.\n */\nexport interface TextTable {\n /** The first line's cells, when it names the columns; `null` when it is data. */\n header: string[] | null;\n /** Each read row, `width` cells long. An empty cell is `\"\"`, kept in its place. */\n rows: string[][];\n /** Where each row came from — row-aligned with `rows`. */\n lines: TableLine[];\n /** How many columns the table has. */\n width: number;\n separator: TableSeparator;\n /** Whether a comma is this table's decimal mark — the convention its numbers are\n * written in, to read them with {@link tableNumber} and to say back to the reader. */\n decimalComma: boolean;\n /** Lines that are not a row of this table: a title above it, a total under it, a\n * row with a cell too many or too few. */\n unread: TableLine[];\n}\n\n/**\n * A pasted or dropped table, read as text — the lexer's file door for a table whose\n * cells are not all numbers (a bank export: a date, a payee, an amount), and the\n * parse under {@link ColumnMapper}.\n *\n * Moved under the same roof as {@link parseTable} because it has the same three\n * questions — what separates the columns, what the comma is, whether the first\n * line is a header — and answering them a second time somewhere else is how the\n * three readers this module replaced came to disagree. Kurvenschmiede's columns\n * input and keksdose's bank-file import both read a table whose columns the reader\n * then assigns, and both state what was assumed; this is that reading, once.\n *\n * Where it differs from {@link parseTable}, and why:\n *\n * * **Cells stay text.** Which cell is a number is the caller's question (its\n * column roles), so nothing here converts. {@link tableNumber} reads one in the\n * table's own convention.\n * * **An empty cell keeps its place.** A bank row with no memo has `;;` in it, and\n * dropping that field — which the numeric lexer does — would move the amount into\n * the memo's column. A trailing separator that ends most lines (`1;2;3;`, which\n * spreadsheets write) is a line ending, not a column.\n * * **Quoted cells.** `\"Example Ltd; branch 2\"` is one cell, and `\"\"` inside quotes\n * is a quote. Text has separators in it where numbers never do. A quoted cell that\n * runs over a line break is not joined — the line then has the wrong number of\n * cells and is reported.\n * * **The width is the file's.** The column count most lines share. A line with\n * another count is reported in `unread` rather than padded or cut: a row with a\n * cell too many is a row whose columns have slid, and reading it would put a payee\n * in the amount.\n * * **The separator is voted on**, over the first lines, rather than taken from the\n * second: an export that opens with a title block (a bank's account name and\n * period over the table) would otherwise be read with the title's separator.\n * * **The header is detected by its words.** The numeric lexer's rule — \"a line\n * that is not numbers\" — cannot work where the rows are text too. A first line is\n * a header when one of its cells is a word and every cell under it, as far as the\n * first rows go, is a value (digits and the marks numbers and dates are written\n * with): \"Amount\" over `-12,50`, \"Date\" over `01.02.2026`. A table of words only\n * is read without one; `options.header` says otherwise.\n *\n * Blank lines and `#` banners are skipped, as by the numeric lexer.\n */\nexport function parseTextTable(text: string, options: ParseTextTableOptions = {}): TextTable {\n const content = textLines(text);\n if (!content.length) {\n return { header: null, rows: [], lines: [], width: 0, separator: options.separator ?? \",\", decimalComma: false, unread: [] };\n }\n const separator = options.separator ?? votedSeparator(content);\n const decimalComma =\n options.decimalComma ?? (separator !== \",\" && DECIMAL_COMMA.test(content.map((entry) => entry.line).join(\"\\n\")));\n\n // A trailing separator is a line ending only where it is the file's habit: in a\n // file where most lines end in one. Elsewhere it closes an empty last cell.\n const trailing =\n separator !== \" \" && content.filter(({ line }) => line.endsWith(separator)).length * 2 > content.length;\n const split = content.map(({ line, number }) => {\n const cells = cellsOf(line, separator);\n if (trailing && line.endsWith(separator) && cells[cells.length - 1] === \"\") cells.pop();\n return { cells, entry: { line: number, text: line } };\n });\n\n const width = commonWidth(split.map(({ cells }) => cells.length));\n const first = split.findIndex(({ cells }) => cells.length === width);\n const unread: TableLine[] = split.slice(0, first).map(({ entry }) => entry);\n const body = split.slice(first);\n const fitting = body.filter(({ cells }) => cells.length === width);\n const header = options.header ?? looksLikeHeader(fitting[0].cells, fitting.slice(1, 21).map(({ cells }) => cells));\n\n const rows: string[][] = [];\n const lines: TableLine[] = [];\n body.forEach(({ cells, entry }, index) => {\n if (index === 0 && header) return;\n if (cells.length !== width) {\n unread.push(entry);\n return;\n }\n rows.push(cells);\n lines.push(entry);\n });\n\n return { header: header ? body[0].cells : null, rows, lines, width, separator, decimalComma, unread };\n}\n\n/**\n * One cell of a {@link TextTable} as a number, read in the table's convention.\n *\n * Not {@link cellNumber}, which reads every comma as a decimal mark — right for a\n * cell typed on its own, wrong for a cell of a file written with decimal points,\n * where `1,234` is a grouped thousand and reading it as 1.234 would be off by a\n * factor of a thousand without a word. Here a comma is a decimal mark only in a\n * table whose convention it is, and a cell with both marks is `NaN` either way: the\n * lexer does not read grouped numbers, it reports them. An empty cell is `NaN` too —\n * a hole in the table, not a zero.\n */\nexport function tableNumber(cell: string, decimalComma: boolean): number {\n const trimmed = cell.trim();\n if (trimmed === \"\") return Number.NaN;\n return Number(decimalComma ? point(trimmed) : trimmed);\n}\n\nconst SEPARATOR_TRUST: readonly TableSeparator[] = [\"\\t\", \";\", \",\", \" \"];\n\n/** {@link contentLines} for a table of text: spaces come off the ends, but a TAB\n * stays — in a tab-separated export a leading tab is an empty first cell, and\n * trimming it would slide the whole row one column to the left. */\nfunction textLines(text: string): { line: string; number: number }[] {\n return text\n .split(/\\r?\\n/)\n .map((line, index) => ({ line: line.replace(/^ +| +$/g, \"\"), number: index + 1 }))\n .filter(({ line }) => line.trim() !== \"\" && !line.trimStart().startsWith(\"#\"));\n}\n\n/** The separator most of the first lines are written with — a title block above the\n * table loses the vote to the table under it. A tie goes to the more trusted one. */\nfunction votedSeparator(lines: { line: string }[]): TableSeparator {\n const votes = new Map<TableSeparator, number>();\n for (const { line } of lines.slice(0, 50)) {\n const found = separatorOf(withoutQuoted(line));\n const named: TableSeparator = typeof found === \"string\" ? (found as TableSeparator) : \" \";\n votes.set(named, (votes.get(named) ?? 0) + 1);\n }\n let best: TableSeparator = SEPARATOR_TRUST[0];\n let bestCount = -1;\n for (const candidate of SEPARATOR_TRUST) {\n const count = votes.get(candidate) ?? 0;\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return best;\n}\n\n/** A line with its quoted parts emptied, so a separator inside quotes does not vote. */\nconst withoutQuoted = (line: string) => line.replace(/\"(?:[^\"]|\"\")*\"/g, '\"\"');\n\n/**\n * One line into cells: trimmed, quotes taken off, empty cells kept in their place.\n * A run of whitespace is one separator and makes no empty cells.\n */\nfunction cellsOf(line: string, separator: TableSeparator): string[] {\n if (separator === \" \") return line.split(/\\s+/).filter((field) => field !== \"\").map(unquote);\n const cells: string[] = [];\n let cell = \"\";\n let quoted = false;\n let started = false;\n for (let index = 0; index < line.length; index += 1) {\n const char = line[index];\n if (quoted) {\n if (char === '\"' && line[index + 1] === '\"') {\n cell += '\"';\n index += 1;\n } else if (char === '\"') {\n quoted = false;\n } else {\n cell += char;\n }\n } else if (char === separator) {\n cells.push(cell.trim());\n cell = \"\";\n started = false;\n } else if (char === '\"' && !started) {\n quoted = true;\n started = true;\n cell = \"\";\n } else {\n if (char.trim() !== \"\") started = true;\n cell += char;\n }\n }\n cells.push(cell.trim());\n return cells;\n}\n\n/** A whitespace-separated field with its quotes off. */\nfunction unquote(field: string): string {\n return field.length >= 2 && field.startsWith('\"') && field.endsWith('\"') ? field.slice(1, -1).replace(/\"\"/g, '\"') : field;\n}\n\n/** The column count most lines share; a tie goes to the wider table. */\nfunction commonWidth(counts: number[]): number {\n const tally = new Map<number, number>();\n for (const count of counts) tally.set(count, (tally.get(count) ?? 0) + 1);\n let width = 0;\n let best = 0;\n for (const [count, times] of tally) {\n if (times > best || (times === best && count > width)) {\n width = count;\n best = times;\n }\n }\n return width;\n}\n\n/** A cell with a letter in it — a name, not a value. */\nconst isWord = (cell: string) => /\\p{L}/u.test(cell);\n/** A cell made of digits and the marks numbers, dates and times are written with. */\nconst isValue = (cell: string) => /\\d/.test(cell) && !isWord(cell);\n\n/**\n * Whether a first line names the columns under it. A word over a column of values\n * says so; with no rows to compare against, a line of words alone does.\n */\nfunction looksLikeHeader(first: string[], below: string[][]): boolean {\n if (!below.length) return first.some((cell) => cell !== \"\") && first.every((cell) => cell === \"\" || isWord(cell));\n return first.some((cell, column) => {\n if (!isWord(cell)) return false;\n const values = below.map((row) => row[column]).filter((value) => value !== \"\");\n return values.length > 0 && values.every(isValue);\n });\n}\n"],"mappings":"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","names":[]}
|