@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.
Files changed (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/phone.ts"],"sourcesContent":["/**\n * Phone numbers — E.164 in, grouped out — for kastlan's contacts, without a\n * libphonenumber dependency.\n *\n * kastlan stored whatever was typed (\"079 …\", \"+41 (0)79 …\", \"0041 79 …\") and could\n * neither dial nor de-duplicate it. What it needs is one canonical form to store —\n * E.164, \"+41210000001\" — and a readable one to show. libphonenumber gets both right\n * for every country, at ~150 kB of metadata in every app's bundle; the kit's apps are\n * Swiss and their neighbours, so this is **light per-country rules** for the six that\n * matter and a generic shape check for the rest:\n *\n * - **CH, LI, DE, AT, FR, IT**: the national trunk prefix is dropped (a leading 0 —\n * but NOT for Italy, whose fixed-line numbers keep their 0 after +39, and not for\n * Liechtenstein, which has none), and the remaining national number has to be a\n * plausible length for the country.\n * - **Anything else** typed with its own \"+\" (or \"00\"): the digits, 8 to 15 in all,\n * are taken as an E.164 number as they stand.\n *\n * **The limitation:** a plausible length is not a valid number. There is no check of\n * area codes or number ranges, so a well-formed number that does not exist passes, and\n * a German or Austrian number (whose area codes run from one to five digits) is\n * grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown in one block. A number these rules cannot read is not an error —\n * {@link parsePhone} returns `null` and the field keeps the text as typed.\n */\n\n/** The countries with their own rules. */\nexport type PhoneCountryCode = \"CH\" | \"LI\" | \"DE\" | \"AT\" | \"FR\" | \"IT\";\n\n/** A country with rules, or `\"other\"`: a number typed with its own \"+\". */\nexport type PhoneCountry = PhoneCountryCode | \"other\";\n\ninterface CountryRule {\n /** The country calling code, without the \"+\". */\n dial: string;\n /** A national number starts with a trunk 0 that is dropped after the country code. */\n trunk: boolean;\n /** The national significant number itself may start with 0 (Italy's fixed lines). */\n zeroStart?: boolean;\n /** The national significant number's length, inclusive. */\n min: number;\n max: number;\n /** Groups for display, given the national significant number. */\n group: (nsn: string) => string[];\n}\n\n/** `nsn` cut into the given sizes; whatever is left is one last group. */\nfunction cut(nsn: string, sizes: number[]): string[] {\n const out: string[] = [];\n let at = 0;\n for (const size of sizes) {\n if (at >= nsn.length) break;\n out.push(nsn.slice(at, at + size));\n at += size;\n }\n if (at < nsn.length) out.push(nsn.slice(at));\n return out;\n}\n\nconst RULES: Record<PhoneCountryCode, CountryRule> = {\n // 9 digits, 2-3-2-2 (\"21 000 00 01\"); the 08xx/09xx service numbers 3-3-3.\n CH: {\n dial: \"41\",\n trunk: true,\n min: 9,\n max: 9,\n group: (n) => (/^[89]/.test(n) ? cut(n, [3, 3, 3]) : cut(n, [2, 3, 2, 2])),\n },\n // 7 digits, 3-2-2 (\"100 00 01\"), and 9 for some mobile ranges (3-3-3). No trunk prefix.\n LI: {\n dial: \"423\",\n trunk: false,\n min: 7,\n max: 9,\n group: (n) => (n.length === 7 ? cut(n, [3, 2, 2]) : n.length === 9 ? cut(n, [3, 3, 3]) : [n]),\n },\n // Area codes of two to five digits: only the mobile prefixes and the four\n // two-digit cities are split; anything else is one block rather than a wrong split.\n DE: {\n dial: \"49\",\n trunk: true,\n min: 6,\n max: 13,\n group: (n) =>\n /^15/.test(n)\n ? cut(n, [4])\n : /^1[67]/.test(n)\n ? cut(n, [3])\n : /^(30|40|69|89)/.test(n)\n ? cut(n, [2])\n : [n],\n },\n // Vienna is \"1\"; mobile numbers start 6xx; other area codes vary, so one block.\n AT: {\n dial: \"43\",\n trunk: true,\n min: 6,\n max: 13,\n group: (n) => (/^6/.test(n) ? cut(n, [3]) : /^1/.test(n) ? cut(n, [1]) : [n]),\n },\n // 9 digits, 1-2-2-2-2 (\"1 99 00 00 01\").\n FR: { dial: \"33\", trunk: true, min: 9, max: 9, group: (n) => cut(n, [1, 2, 2, 2, 2]) },\n // The 0 of a fixed line is part of the number (\"+39 06 …\"); mobiles start with 3.\n IT: {\n dial: \"39\",\n trunk: false,\n zeroStart: true,\n min: 6,\n max: 11,\n group: (n) => (/^3/.test(n) ? cut(n, [3, 3]) : /^0[26]/.test(n) ? cut(n, [2, 4]) : [n]),\n },\n};\n\n/** The country calling codes with rules, \"+\" left off: `{ CH: \"41\", … }`. */\nexport const PHONE_DIAL_CODES: Readonly<Record<PhoneCountryCode, string>> = Object.freeze({\n CH: RULES.CH.dial,\n LI: RULES.LI.dial,\n DE: RULES.DE.dial,\n AT: RULES.AT.dial,\n FR: RULES.FR.dial,\n IT: RULES.IT.dial,\n});\n\n/** The countries with rules, in the order a picker offers them by default. */\nexport const PHONE_COUNTRIES: readonly PhoneCountryCode[] = Object.freeze([\"CH\", \"LI\", \"DE\", \"AT\", \"FR\", \"IT\"]);\n\n/** Longest dial code first, so \"+423\" is Liechtenstein before anything shorter. */\nconst BY_DIAL = [...PHONE_COUNTRIES].sort((a, b) => RULES[b].dial.length - RULES[a].dial.length);\n\n/** What {@link parsePhone} reads out of a number. */\nexport interface ParsedPhone {\n /** The number in E.164: \"+\", the country code, the national number. */\n e164: string;\n /** The country whose rules read it, or `null` for a generic \"+…\" number. */\n country: PhoneCountryCode | null;\n /** The national significant number — the digits after the country code. For a\n * generic number, every digit after the \"+\" (the country code is not split off). */\n national: string;\n}\n\n/** A national significant number that passes `rule`, or `null`. */\nfunction national(digits: string, rule: CountryRule): string | null {\n const nsn = rule.trunk && digits.startsWith(\"0\") ? digits.slice(1) : digits;\n // A national number starts with 0 only where 0 is part of it (Italy's fixed lines).\n if (nsn.startsWith(\"0\") && !rule.zeroStart) return null;\n if (nsn.length < rule.min || nsn.length > rule.max) return null;\n return nsn;\n}\n\n/**\n * Read a typed number. With a leading \"+\" or \"00\" the number carries its own country\n * code — one of the six with rules, or any other (8–15 digits in all, the E.164\n * limit). Without one it is a national number of `country` (default `\"CH\"`), its\n * trunk 0 optional; with `country=\"other\"` a number without \"+\" cannot be read.\n *\n * Spaces, hyphens, dots, slashes and parentheses are ignored (\"(030) 1234567\"), and\n * so is the \"(0)\" of \"+41 (0)21 …\". Anything else — a letter, an extension (\"ext. 12\"), a note —\n * and the result is `null`: the text is not a number these rules can read, and a field\n * keeps it as it is rather than guess.\n */\nexport function parsePhone(input: string, country: PhoneCountry = \"CH\"): ParsedPhone | null {\n const text = input.trim();\n if (text === \"\" || !/^\\+?[\\d\\s().\\-/]+$/.test(text)) return null;\n let digits = text.replace(/\\D/g, \"\");\n let international = text.startsWith(\"+\");\n if (!international && digits.startsWith(\"00\")) {\n international = true;\n digits = digits.slice(2);\n }\n if (international) {\n // \"+41 (0)21 …\": the bracketed trunk prefix written for callers inside the country.\n digits = text.replace(/\\(\\s*0\\s*\\)/g, \"\").replace(/\\D/g, \"\").replace(/^00/, \"\");\n if (digits.startsWith(\"0\")) return null;\n const known = BY_DIAL.find((c) => digits.startsWith(RULES[c].dial));\n if (known) {\n const rule = RULES[known];\n const nsn = national(digits.slice(rule.dial.length), rule);\n return nsn === null ? null : { e164: `+${rule.dial}${nsn}`, country: known, national: nsn };\n }\n if (digits.length < 8 || digits.length > 15) return null;\n return { e164: `+${digits}`, country: null, national: digits };\n }\n if (country === \"other\") return null;\n const rule = RULES[country];\n const nsn = national(digits, rule);\n return nsn === null ? null : { e164: `+${rule.dial}${nsn}`, country, national: nsn };\n}\n\n/** The national number grouped by its country's rules (\"21 000 00 01\"). */\nexport function formatNationalPhone(nsn: string, country: PhoneCountryCode): string {\n return RULES[country].group(nsn).join(\" \");\n}\n\n/**\n * A number for display: an E.164 number (or anything {@link parsePhone} reads with a\n * \"+\") grouped by its country's rules — \"+41 21 000 00 01\" — and a generic one as\n * \"+\" and its digits. Anything it cannot read comes back exactly as it went in, so a\n * list of contacts can format every row without first sorting out the free text.\n */\nexport function formatPhone(value: string): string {\n const parsed = value.trim().startsWith(\"+\") ? parsePhone(value, \"other\") : null;\n if (!parsed) return value;\n if (!parsed.country) return parsed.e164;\n return `+${RULES[parsed.country].dial} ${formatNationalPhone(parsed.national, parsed.country)}`;\n}\n\n/** Whether `value` is in E.164 shape: \"+\" and 8–15 digits, nothing else. Shape only —\n * what {@link PhoneInput} emits for every number it could read. */\nexport function isE164(value: string): boolean {\n return /^\\+[1-9]\\d{7,14}$/.test(value);\n}\n"],"mappings":"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","names":["rule","nsn"]}
@@ -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 };
@@ -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
- splitRow
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":"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","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":[]}