@dou-ui/sheet 0.0.0-stage → 0.1.0-alpha.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 (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/cell-types.js +20 -0
  4. package/dist/cell-types.js.map +1 -0
  5. package/dist/code/code-formula-bar.js +6 -0
  6. package/dist/code/code-formula-bar.js.map +1 -0
  7. package/dist/code/code-formula-bar.vue_vue_type_script_setup_true_lang.js +192 -0
  8. package/dist/code/code-formula-bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  9. package/dist/code/index.js +12 -0
  10. package/dist/code/index.js.map +1 -0
  11. package/dist/code/language.js +51 -0
  12. package/dist/code/language.js.map +1 -0
  13. package/dist/code/style.d.ts +1 -0
  14. package/dist/code/style.js +2 -0
  15. package/dist/collaboration/anchor.js +143 -0
  16. package/dist/collaboration/anchor.js.map +1 -0
  17. package/dist/collaboration/index.js +770 -0
  18. package/dist/collaboration/index.js.map +1 -0
  19. package/dist/collaboration/peers.js +87 -0
  20. package/dist/collaboration/peers.js.map +1 -0
  21. package/dist/collaboration/shared.js +450 -0
  22. package/dist/collaboration/shared.js.map +1 -0
  23. package/dist/components/cell-panel.js +6 -0
  24. package/dist/components/cell-panel.js.map +1 -0
  25. package/dist/components/cell-panel.vue_vue_type_script_setup_true_lang.js +70 -0
  26. package/dist/components/cell-panel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  27. package/dist/components/cell-renders.js +40 -0
  28. package/dist/components/cell-renders.js.map +1 -0
  29. package/dist/components/date-panel.js +6 -0
  30. package/dist/components/date-panel.js.map +1 -0
  31. package/dist/components/date-panel.vue_vue_type_script_setup_true_lang.js +48 -0
  32. package/dist/components/date-panel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  33. package/dist/components/filter-panel.js +6 -0
  34. package/dist/components/filter-panel.js.map +1 -0
  35. package/dist/components/filter-panel.vue_vue_type_script_setup_true_lang.js +259 -0
  36. package/dist/components/filter-panel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  37. package/dist/components/format-dialog.js +6 -0
  38. package/dist/components/format-dialog.js.map +1 -0
  39. package/dist/components/format-dialog.vue_vue_type_script_setup_true_lang.js +432 -0
  40. package/dist/components/format-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/components/format-list.js +6 -0
  42. package/dist/components/format-list.js.map +1 -0
  43. package/dist/components/format-list.vue_vue_type_script_setup_true_lang.js +187 -0
  44. package/dist/components/format-list.vue_vue_type_script_setup_true_lang.js.map +1 -0
  45. package/dist/components/format-presets.js +97 -0
  46. package/dist/components/format-presets.js.map +1 -0
  47. package/dist/components/option-panel.js +6 -0
  48. package/dist/components/option-panel.js.map +1 -0
  49. package/dist/components/option-panel.vue_vue_type_script_setup_true_lang.js +117 -0
  50. package/dist/components/option-panel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  51. package/dist/components/sheet-comment.js +6 -0
  52. package/dist/components/sheet-comment.js.map +1 -0
  53. package/dist/components/sheet-comment.vue_vue_type_script_setup_true_lang.js +104 -0
  54. package/dist/components/sheet-comment.vue_vue_type_script_setup_true_lang.js.map +1 -0
  55. package/dist/components/sheet-formula-bar.js +6 -0
  56. package/dist/components/sheet-formula-bar.js.map +1 -0
  57. package/dist/components/sheet-formula-bar.vue_vue_type_script_setup_true_lang.js +124 -0
  58. package/dist/components/sheet-formula-bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  59. package/dist/components/sheet-link.js +6 -0
  60. package/dist/components/sheet-link.js.map +1 -0
  61. package/dist/components/sheet-link.vue_vue_type_script_setup_true_lang.js +249 -0
  62. package/dist/components/sheet-link.vue_vue_type_script_setup_true_lang.js.map +1 -0
  63. package/dist/components/sheet-menu.js +6 -0
  64. package/dist/components/sheet-menu.js.map +1 -0
  65. package/dist/components/sheet-menu.vue_vue_type_script_setup_true_lang.js +115 -0
  66. package/dist/components/sheet-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
  67. package/dist/components/sheet-name-box.js +6 -0
  68. package/dist/components/sheet-name-box.js.map +1 -0
  69. package/dist/components/sheet-name-box.vue_vue_type_script_setup_true_lang.js +91 -0
  70. package/dist/components/sheet-name-box.vue_vue_type_script_setup_true_lang.js.map +1 -0
  71. package/dist/components/sheet-note.js +6 -0
  72. package/dist/components/sheet-note.js.map +1 -0
  73. package/dist/components/sheet-note.vue_vue_type_script_setup_true_lang.js +146 -0
  74. package/dist/components/sheet-note.vue_vue_type_script_setup_true_lang.js.map +1 -0
  75. package/dist/components/sheet-outline.js +6 -0
  76. package/dist/components/sheet-outline.js.map +1 -0
  77. package/dist/components/sheet-outline.vue_vue_type_script_setup_true_lang.js +237 -0
  78. package/dist/components/sheet-outline.vue_vue_type_script_setup_true_lang.js.map +1 -0
  79. package/dist/components/sheet-tabs.js +6 -0
  80. package/dist/components/sheet-tabs.js.map +1 -0
  81. package/dist/components/sheet-tabs.vue_vue_type_script_setup_true_lang.js +487 -0
  82. package/dist/components/sheet-tabs.vue_vue_type_script_setup_true_lang.js.map +1 -0
  83. package/dist/components/sheet-toolbar.js +6 -0
  84. package/dist/components/sheet-toolbar.js.map +1 -0
  85. package/dist/components/sheet-toolbar.vue_vue_type_script_setup_true_lang.js +1562 -0
  86. package/dist/components/sheet-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  87. package/dist/components/validation-dialog.js +6 -0
  88. package/dist/components/validation-dialog.js.map +1 -0
  89. package/dist/components/validation-dialog.vue_vue_type_script_setup_true_lang.js +370 -0
  90. package/dist/components/validation-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  91. package/dist/context.js +12 -0
  92. package/dist/context.js.map +1 -0
  93. package/dist/core/address.js +256 -0
  94. package/dist/core/address.js.map +1 -0
  95. package/dist/core/axis.js +72 -0
  96. package/dist/core/axis.js.map +1 -0
  97. package/dist/core/cell-type.js +30 -0
  98. package/dist/core/cell-type.js.map +1 -0
  99. package/dist/core/clipboard.js +141 -0
  100. package/dist/core/clipboard.js.map +1 -0
  101. package/dist/core/commands.js +63 -0
  102. package/dist/core/commands.js.map +1 -0
  103. package/dist/core/comments.js +74 -0
  104. package/dist/core/comments.js.map +1 -0
  105. package/dist/core/conditional.js +362 -0
  106. package/dist/core/conditional.js.map +1 -0
  107. package/dist/core/csv.js +191 -0
  108. package/dist/core/csv.js.map +1 -0
  109. package/dist/core/data.js +118 -0
  110. package/dist/core/data.js.map +1 -0
  111. package/dist/core/document.js +187 -0
  112. package/dist/core/document.js.map +1 -0
  113. package/dist/core/extension.js +60 -0
  114. package/dist/core/extension.js.map +1 -0
  115. package/dist/core/file.js +336 -0
  116. package/dist/core/file.js.map +1 -0
  117. package/dist/core/fill.js +153 -0
  118. package/dist/core/fill.js.map +1 -0
  119. package/dist/core/find.js +42 -0
  120. package/dist/core/find.js.map +1 -0
  121. package/dist/core/format.js +404 -0
  122. package/dist/core/format.js.map +1 -0
  123. package/dist/core/group.js +134 -0
  124. package/dist/core/group.js.map +1 -0
  125. package/dist/core/history.js +68 -0
  126. package/dist/core/history.js.map +1 -0
  127. package/dist/core/index.js +17 -0
  128. package/dist/core/link.js +34 -0
  129. package/dist/core/link.js.map +1 -0
  130. package/dist/core/merge.js +105 -0
  131. package/dist/core/merge.js.map +1 -0
  132. package/dist/core/model.js +48 -0
  133. package/dist/core/model.js.map +1 -0
  134. package/dist/core/nav.js +44 -0
  135. package/dist/core/nav.js.map +1 -0
  136. package/dist/core/paint.js +106 -0
  137. package/dist/core/paint.js.map +1 -0
  138. package/dist/core/plain-cell-types.js +54 -0
  139. package/dist/core/plain-cell-types.js.map +1 -0
  140. package/dist/core/protect.js +246 -0
  141. package/dist/core/protect.js.map +1 -0
  142. package/dist/core/selection.js +37 -0
  143. package/dist/core/selection.js.map +1 -0
  144. package/dist/core/transaction.js +1582 -0
  145. package/dist/core/transaction.js.map +1 -0
  146. package/dist/core/validation.js +135 -0
  147. package/dist/core/validation.js.map +1 -0
  148. package/dist/core/versions.js +161 -0
  149. package/dist/core/versions.js.map +1 -0
  150. package/dist/export/color.js +36 -0
  151. package/dist/export/color.js.map +1 -0
  152. package/dist/export/draw.js +316 -0
  153. package/dist/export/draw.js.map +1 -0
  154. package/dist/export/index.js +390 -0
  155. package/dist/export/index.js.map +1 -0
  156. package/dist/formula/edit.js +210 -0
  157. package/dist/formula/edit.js.map +1 -0
  158. package/dist/formula/engine.js +526 -0
  159. package/dist/formula/engine.js.map +1 -0
  160. package/dist/formula/error.js +39 -0
  161. package/dist/formula/error.js.map +1 -0
  162. package/dist/formula/functions.js +1493 -0
  163. package/dist/formula/functions.js.map +1 -0
  164. package/dist/formula/help.js +408 -0
  165. package/dist/formula/help.js.map +1 -0
  166. package/dist/formula/index.js +7 -0
  167. package/dist/formula/parse.js +369 -0
  168. package/dist/formula/parse.js.map +1 -0
  169. package/dist/formula/values.js +123 -0
  170. package/dist/formula/values.js.map +1 -0
  171. package/dist/formulas.js +30 -0
  172. package/dist/formulas.js.map +1 -0
  173. package/dist/index.js +39 -0
  174. package/dist/index.js.map +1 -0
  175. package/dist/locale.js +1312 -0
  176. package/dist/locale.js.map +1 -0
  177. package/dist/sheet.js +6 -0
  178. package/dist/sheet.js.map +1 -0
  179. package/dist/sheet.vue_vue_type_script_setup_true_lang.js +7166 -0
  180. package/dist/sheet.vue_vue_type_script_setup_true_lang.js.map +1 -0
  181. package/dist/style.css +1 -0
  182. package/dist/style.d.ts +1 -0
  183. package/dist/style.js +17 -0
  184. package/dist/types/cell-types.d.ts +2 -0
  185. package/dist/types/code/code-formula-bar.vue.d.ts +3 -0
  186. package/dist/types/code/index.d.ts +12 -0
  187. package/dist/types/code/language.d.ts +13 -0
  188. package/dist/types/collaboration/anchor.d.ts +65 -0
  189. package/dist/types/collaboration/index.d.ts +59 -0
  190. package/dist/types/collaboration/peers.d.ts +21 -0
  191. package/dist/types/collaboration/shared.d.ts +75 -0
  192. package/dist/types/components/cell-panel.vue.d.ts +20 -0
  193. package/dist/types/components/cell-renders.d.ts +9 -0
  194. package/dist/types/components/date-panel.vue.d.ts +9 -0
  195. package/dist/types/components/filter-panel.vue.d.ts +21 -0
  196. package/dist/types/components/format-dialog.vue.d.ts +24 -0
  197. package/dist/types/components/format-list.vue.d.ts +25 -0
  198. package/dist/types/components/format-presets.d.ts +30 -0
  199. package/dist/types/components/option-panel.vue.d.ts +13 -0
  200. package/dist/types/components/sheet-comment.vue.d.ts +29 -0
  201. package/dist/types/components/sheet-formula-bar.vue.d.ts +3 -0
  202. package/dist/types/components/sheet-link.vue.d.ts +32 -0
  203. package/dist/types/components/sheet-menu.vue.d.ts +20 -0
  204. package/dist/types/components/sheet-name-box.vue.d.ts +3 -0
  205. package/dist/types/components/sheet-note.vue.d.ts +22 -0
  206. package/dist/types/components/sheet-outline.vue.d.ts +36 -0
  207. package/dist/types/components/sheet-tabs.vue.d.ts +9 -0
  208. package/dist/types/components/sheet-toolbar.vue.d.ts +11 -0
  209. package/dist/types/components/validation-dialog.vue.d.ts +25 -0
  210. package/dist/types/context.d.ts +189 -0
  211. package/dist/types/core/address.d.ts +109 -0
  212. package/dist/types/core/axis.d.ts +23 -0
  213. package/dist/types/core/cell-type.d.ts +157 -0
  214. package/dist/types/core/clipboard.d.ts +48 -0
  215. package/dist/types/core/commands.d.ts +76 -0
  216. package/dist/types/core/comments.d.ts +22 -0
  217. package/dist/types/core/conditional.d.ts +114 -0
  218. package/dist/types/core/csv.d.ts +93 -0
  219. package/dist/types/core/data.d.ts +26 -0
  220. package/dist/types/core/document.d.ts +49 -0
  221. package/dist/types/core/extension.d.ts +177 -0
  222. package/dist/types/core/file.d.ts +301 -0
  223. package/dist/types/core/fill.d.ts +23 -0
  224. package/dist/types/core/find.d.ts +39 -0
  225. package/dist/types/core/format.d.ts +47 -0
  226. package/dist/types/core/group.d.ts +76 -0
  227. package/dist/types/core/history.d.ts +27 -0
  228. package/dist/types/core/index.d.ts +33 -0
  229. package/dist/types/core/link.d.ts +14 -0
  230. package/dist/types/core/merge.d.ts +26 -0
  231. package/dist/types/core/model.d.ts +363 -0
  232. package/dist/types/core/nav.d.ts +15 -0
  233. package/dist/types/core/paint.d.ts +19 -0
  234. package/dist/types/core/plain-cell-types.d.ts +6 -0
  235. package/dist/types/core/protect.d.ts +34 -0
  236. package/dist/types/core/selection.d.ts +25 -0
  237. package/dist/types/core/transaction.d.ts +470 -0
  238. package/dist/types/core/validation.d.ts +55 -0
  239. package/dist/types/core/versions.d.ts +38 -0
  240. package/dist/types/export/color.d.ts +7 -0
  241. package/dist/types/export/draw.d.ts +98 -0
  242. package/dist/types/export/index.d.ts +47 -0
  243. package/dist/types/formula/edit.d.ts +62 -0
  244. package/dist/types/formula/engine.d.ts +99 -0
  245. package/dist/types/formula/error.d.ts +30 -0
  246. package/dist/types/formula/functions.d.ts +62 -0
  247. package/dist/types/formula/help.d.ts +12 -0
  248. package/dist/types/formula/index.d.ts +13 -0
  249. package/dist/types/formula/parse.d.ts +92 -0
  250. package/dist/types/formula/values.d.ts +65 -0
  251. package/dist/types/formulas.d.ts +32 -0
  252. package/dist/types/index.d.ts +225 -0
  253. package/dist/types/locale.d.ts +531 -0
  254. package/dist/types/sheet.vue.d.ts +60 -0
  255. package/dist/types/types.d.ts +498 -0
  256. package/dist/types/use-comments.d.ts +47 -0
  257. package/dist/types/use-messages.d.ts +7 -0
  258. package/dist/use-comments.js +130 -0
  259. package/dist/use-comments.js.map +1 -0
  260. package/dist/use-messages.js +29 -0
  261. package/dist/use-messages.js.map +1 -0
  262. package/package.json +93 -4
@@ -0,0 +1,404 @@
1
+ // src/core/format.ts
2
+ var DATE_VALUE = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
3
+ var TIME_VALUE = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
4
+ var DATE_PART = /^(?:yyyy|yy|dddd|ddd|dd|d|mm|m|hh|h|ss|s)/i;
5
+ var patterns = /* @__PURE__ */ new Map();
6
+ /** Splits a section into its digits and the text around them; `null` when it has no digits. */
7
+ function parsePattern(section) {
8
+ let prefix = "";
9
+ let suffix = "";
10
+ let body = "";
11
+ let percent = false;
12
+ for (let i = 0; i < section.length; i++) {
13
+ const char = section[i];
14
+ let text = null;
15
+ if (char === "\"") {
16
+ const close = section.indexOf("\"", i + 1);
17
+ const end = close === -1 ? section.length : close;
18
+ text = section.slice(i + 1, end);
19
+ i = end;
20
+ } else if (char === "\\") text = section[++i] ?? "";
21
+ else if ("0#,.".includes(char) && !suffix) body += char;
22
+ else {
23
+ if (char === "%") percent = true;
24
+ text = char;
25
+ }
26
+ if (text != null) {
27
+ if (body) suffix += text;
28
+ else prefix += text;
29
+ }
30
+ }
31
+ if (!/[0#]/.test(body)) return null;
32
+ const [whole = "", fraction = ""] = body.split(".");
33
+ return {
34
+ prefix,
35
+ suffix,
36
+ fixed: (fraction.match(/0/g) ?? []).length,
37
+ optional: (fraction.match(/#/g) ?? []).length,
38
+ whole: (whole.match(/0/g) ?? []).length,
39
+ grouping: whole.includes(","),
40
+ percent
41
+ };
42
+ }
43
+ function patternOf(section) {
44
+ let pattern = patterns.get(section);
45
+ if (pattern === void 0) {
46
+ pattern = parsePattern(section);
47
+ if (patterns.size > 200) patterns.clear();
48
+ patterns.set(section, pattern);
49
+ }
50
+ return pattern;
51
+ }
52
+ function groupDigits(digits) {
53
+ let out = "";
54
+ for (let i = 0; i < digits.length; i++) {
55
+ if (i && (digits.length - i) % 3 === 0) out += ",";
56
+ out += digits[i];
57
+ }
58
+ return out;
59
+ }
60
+ /** The text of a section without digits, with quotes and `\` marks taken off. */
61
+ function literalOf(section) {
62
+ return section.replace(/"([^"]*)"?|\\(.)/g, (_, quoted, escaped) => quoted ?? escaped ?? "");
63
+ }
64
+ function formatNumber(value, format) {
65
+ const sections = format.split(";");
66
+ const zero = value === 0 && sections.length > 2;
67
+ const negative = value < 0 && sections.length > 1;
68
+ const section = sections[zero ? 2 : negative ? 1 : 0];
69
+ const pattern = patternOf(section);
70
+ if (!pattern) return zero || negative ? literalOf(section) : null;
71
+ const digits = pattern.fixed + pattern.optional;
72
+ const n = Math.abs(value) * (pattern.percent ? 100 : 1);
73
+ const text = String(n).includes("e") ? n.toFixed(digits) : Number(`${Math.round(Number(`${n}e${digits}`))}e-${digits}`).toFixed(digits);
74
+ let [whole = "0", fraction = ""] = text.split(".");
75
+ while (fraction.length > pattern.fixed && fraction.endsWith("0")) fraction = fraction.slice(0, -1);
76
+ whole = whole.replace(/^0+/, "").padStart(pattern.whole, "0");
77
+ if (pattern.grouping) whole = groupDigits(whole);
78
+ const sign = value < 0 && !negative && Number(text) !== 0 ? "-" : "";
79
+ const number = fraction ? `${whole}.${fraction}` : whole || (pattern.fixed ? "0" : "");
80
+ return `${sign}${pattern.prefix}${number}${pattern.suffix}`;
81
+ }
82
+ var dateTokens = /* @__PURE__ */ new Map();
83
+ /** A date format as parts and the text between them. */
84
+ function tokensOf(format) {
85
+ let tokens = dateTokens.get(format);
86
+ if (tokens) return tokens;
87
+ tokens = [];
88
+ for (let i = 0; i < format.length;) {
89
+ const char = format[i];
90
+ if (char === "\"") {
91
+ const close = format.indexOf("\"", i + 1);
92
+ const end = close === -1 ? format.length : close;
93
+ tokens.push({ text: format.slice(i + 1, end) });
94
+ i = end + 1;
95
+ continue;
96
+ }
97
+ if (char === "\\") {
98
+ tokens.push({ text: format[i + 1] ?? "" });
99
+ i += 2;
100
+ continue;
101
+ }
102
+ const part = DATE_PART.exec(format.slice(i))?.[0];
103
+ if (part) {
104
+ tokens.push({ part: part.toLowerCase() });
105
+ i += part.length;
106
+ } else {
107
+ tokens.push({ text: char });
108
+ i++;
109
+ }
110
+ }
111
+ const parts = tokens.filter((t) => "part" in t);
112
+ parts.forEach((t, i) => {
113
+ if (t.part[0] !== "m") return;
114
+ if (parts[i - 1]?.part[0] === "h" || parts[i + 1]?.part[0] === "s") t.part = t.part === "mm" ? "nn" : "n";
115
+ });
116
+ if (dateTokens.size > 200) dateTokens.clear();
117
+ dateTokens.set(format, tokens);
118
+ return tokens;
119
+ }
120
+ /** Whether a format has date parts, outside quoted and escaped text. */
121
+ var isDateFormat = (format) => /[ymdhs]/i.test(format.replace(/"[^"]*"?|\\./g, ""));
122
+ var weekdayNames = /* @__PURE__ */ new Map();
123
+ function weekday(y, m, d, style, locale) {
124
+ const key = `${locale ?? ""}|${style}`;
125
+ let names = weekdayNames.get(key);
126
+ if (!names) {
127
+ try {
128
+ names = new Intl.DateTimeFormat(locale, {
129
+ weekday: style,
130
+ timeZone: "UTC"
131
+ });
132
+ } catch {
133
+ names = new Intl.DateTimeFormat(void 0, {
134
+ weekday: style,
135
+ timeZone: "UTC"
136
+ });
137
+ }
138
+ weekdayNames.set(key, names);
139
+ }
140
+ return names.format(new Date(Date.UTC(y, m - 1, d)));
141
+ }
142
+ function formatDate(value, format, locale) {
143
+ let y = "";
144
+ let mo = "";
145
+ let d = "";
146
+ let h;
147
+ let mi;
148
+ let s;
149
+ const date = DATE_VALUE.exec(value);
150
+ if (date) [, y = "", mo = "", d = "", h = "00", mi = "00", s = "00"] = date;
151
+ else {
152
+ const time = TIME_VALUE.exec(value);
153
+ if (!time) return null;
154
+ [, h = "00", mi = "00", s = "00"] = time;
155
+ }
156
+ let out = "";
157
+ for (const token of tokensOf(format)) {
158
+ if ("text" in token) {
159
+ out += token.text;
160
+ continue;
161
+ }
162
+ const { part } = token;
163
+ if (!date && "ymd".includes(part[0])) return null;
164
+ switch (part) {
165
+ case "yyyy":
166
+ out += y;
167
+ break;
168
+ case "yy":
169
+ out += y.slice(2);
170
+ break;
171
+ case "mm":
172
+ out += mo;
173
+ break;
174
+ case "m":
175
+ out += Number(mo);
176
+ break;
177
+ case "dddd":
178
+ case "ddd":
179
+ out += weekday(+y, +mo, +d, part === "ddd" ? "short" : "long", locale);
180
+ break;
181
+ case "dd":
182
+ out += d;
183
+ break;
184
+ case "d":
185
+ out += Number(d);
186
+ break;
187
+ case "hh":
188
+ out += h;
189
+ break;
190
+ case "h":
191
+ out += Number(h);
192
+ break;
193
+ case "nn":
194
+ out += mi;
195
+ break;
196
+ case "n":
197
+ out += Number(mi);
198
+ break;
199
+ case "ss":
200
+ out += s;
201
+ break;
202
+ default: out += Number(s);
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+ /** Up to 15 significant digits, so 0.1 + 0.2 shows as 0.3. */
208
+ function generalNumber(value) {
209
+ if (!Number.isFinite(value)) return String(value);
210
+ const rounded = Number(value.toPrecision(15));
211
+ const abs = Math.abs(rounded);
212
+ if (abs !== 0 && (abs >= 0x38d7ea4c68000 || abs < 1e-9)) return rounded.toExponential().replace("e+", "E+");
213
+ return String(rounded);
214
+ }
215
+ /**
216
+ * A value as the cell shows it, with its format. `locale` names the
217
+ * language for weekdays (`ddd`, `dddd`).
218
+ */
219
+ function formatValue(value, format, locale) {
220
+ if (value == null) return "";
221
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
222
+ if (format === "@") return String(value);
223
+ if (typeof value === "number") {
224
+ if (format && !isDateFormat(format)) {
225
+ const text = formatNumber(value, format);
226
+ if (text != null) return text;
227
+ }
228
+ return generalNumber(value);
229
+ }
230
+ if (format && isDateFormat(format)) return formatDate(value, format, locale) ?? value;
231
+ return textOf(value);
232
+ }
233
+ /** Text as it shows: `'=…` is text starting with `=`, shown without its mark. */
234
+ var textOf = (value) => value.startsWith("'=") ? value.slice(1) : value;
235
+ /**
236
+ * A number without a format, in at most `chars` characters, for a cell
237
+ * too narrow for all its digits: fewer decimals first, then `1.2E+11`.
238
+ * Only what the cell shows changes, never the value.
239
+ */
240
+ function fitNumber(value, chars) {
241
+ const text = generalNumber(value);
242
+ if (text.length <= chars || !Number.isFinite(value)) return text;
243
+ if (!text.includes("E")) {
244
+ const point = text.indexOf(".");
245
+ if (point !== -1 && point <= chars) {
246
+ const decimals = Math.max(0, chars - point - 1);
247
+ const rounded = String(Number(value.toFixed(decimals)));
248
+ if (rounded.length <= chars && rounded !== "0" && rounded !== "-0") return rounded;
249
+ }
250
+ }
251
+ let shortest = text;
252
+ for (let digits = 14; digits >= 0; digits--) {
253
+ const [mantissa, exponent] = value.toExponential(digits).split("e");
254
+ const out = `${mantissa.includes(".") ? mantissa.replace(/\.?0+$/, "") : mantissa}E${exponent}`;
255
+ shortest = out;
256
+ if (out.length <= chars) return out;
257
+ }
258
+ return shortest;
259
+ }
260
+ /**
261
+ * A number format with `by` more digits after the point, or fewer when
262
+ * `by` is below 0, as the toolbar's buttons change it: `0.00` → `0.000`,
263
+ * `#,##0` → `#,##0.0`, `0.0%` → `0%`. Without a format it starts from the
264
+ * digits `value` shows. Date, time and text formats stay as they are.
265
+ */
266
+ function changeDecimals(format, value, by) {
267
+ if (!format) {
268
+ const text = typeof value === "number" ? generalNumber(value) : "";
269
+ const point = text.includes("E") ? -1 : text.indexOf(".");
270
+ const digits = Math.max(0, (point === -1 ? 0 : text.length - point - 1) + by);
271
+ return digits ? `0.${"0".repeat(digits)}` : "0";
272
+ }
273
+ if (format === "@" || isDateFormat(format)) return format;
274
+ return format.split(";").map((section) => sectionDecimals(section, by)).join(";");
275
+ }
276
+ /** One section of a number format with more or fewer digits after the point. */
277
+ function sectionDecimals(section, by) {
278
+ let start = -1;
279
+ for (let i = 0; i < section.length; i++) {
280
+ const char = section[i];
281
+ if (char === "\"") {
282
+ const close = section.indexOf("\"", i + 1);
283
+ i = close === -1 ? section.length : close;
284
+ } else if (char === "\\") i++;
285
+ else if (char === "0" || char === "#") {
286
+ start = i;
287
+ break;
288
+ }
289
+ }
290
+ if (start === -1) return section;
291
+ let end = start;
292
+ while (end < section.length && "0#,.".includes(section[end])) end++;
293
+ const [whole = "", fraction = ""] = section.slice(start, end).split(".");
294
+ const digits = by > 0 ? fraction + "0".repeat(by) : fraction.slice(0, Math.max(0, fraction.length + by));
295
+ return section.slice(0, start) + (digits ? `${whole}.${digits}` : whole) + section.slice(end);
296
+ }
297
+ /**
298
+ * How a value lines up in its cell unless its style says otherwise:
299
+ * numbers, dates and times to the right, `TRUE` and `FALSE` in the middle,
300
+ * text to the left.
301
+ */
302
+ function alignOf(value) {
303
+ if (typeof value === "number") return "right";
304
+ if (typeof value === "boolean") return "center";
305
+ if (typeof value === "string" && (DATE_VALUE.test(value) || TIME_VALUE.test(value))) return "right";
306
+ return "left";
307
+ }
308
+ var NUMBER = /^[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)?(?:\.\d+)?(?:e[-+]?\d+)?$/i;
309
+ var PLAIN_NUMBER = /^(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?$/;
310
+ var TYPED_DATE = /^(\d{4})([-/.])(\d{1,2})\2(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/;
311
+ var TYPED_TIME = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/;
312
+ var CURRENCY = /^([-+]?)([¥$€£])\s?([-+]?)(.+)$/;
313
+ var pad = (n) => n.padStart(2, "0");
314
+ /** A date part's format letter, doubled when it was typed with a leading zero. */
315
+ var two = (part, letter) => part.startsWith("0") ? letter + letter : letter;
316
+ /** `HH:mm[:ss]` and the format that shows it as typed, or `null` when it isn't a time. */
317
+ function readTime(h, m, s) {
318
+ if (Number(h) > 23 || Number(m) > 59 || s != null && Number(s) > 59) return null;
319
+ return {
320
+ value: `${pad(h)}:${m}${s != null ? `:${s}` : ""}`,
321
+ format: `${h.startsWith("0") ? "hh" : "h"}:mm${s != null ? ":ss" : ""}`,
322
+ canonical: h.length === 2
323
+ };
324
+ }
325
+ /**
326
+ * Reads typed text: numbers (with thousands commas, `12%`, `¥1,200.50`),
327
+ * `TRUE` and `FALSE`, dates (`2026-10-31`, `2026/3/5`, `2026.3.5`, with a
328
+ * time or not), times (`9:30`), formulas (`=…`) and text. A leading `'`
329
+ * keeps the rest as text. Dates and times are written the standard way
330
+ * (`2026-03-05`, `09:30`); when typed another way, the format shows them
331
+ * as typed.
332
+ */
333
+ function parseInput(text) {
334
+ if (text === "") return { value: null };
335
+ if (text.startsWith("'")) return { value: text.startsWith("'=") ? text : text.slice(1) };
336
+ if (text.startsWith("=") && text.length > 1) return { value: text };
337
+ const trimmed = text.trim();
338
+ if (/^\S/.test(trimmed) && trimmed !== "." && /\d/.test(trimmed)) {
339
+ if (NUMBER.test(trimmed)) return { value: Number(trimmed.replaceAll(",", "")) };
340
+ if (trimmed.endsWith("%") && NUMBER.test(trimmed.slice(0, -1))) {
341
+ const n = Number(trimmed.slice(0, -1).replaceAll(",", "")) / 100;
342
+ const decimals = /\.(\d+)%$/.exec(trimmed)?.[1].length ?? 0;
343
+ const format = decimals ? `0.${"0".repeat(decimals)}%` : "0%";
344
+ return {
345
+ value: Number(n.toPrecision(15)),
346
+ format
347
+ };
348
+ }
349
+ const money = CURRENCY.exec(trimmed);
350
+ if (money && !(money[1] && money[3])) {
351
+ const [, before, symbol, after, digits] = money;
352
+ const number = PLAIN_NUMBER.exec(digits);
353
+ if (number) {
354
+ const n = Number(digits.replaceAll(",", ""));
355
+ const decimals = number[1]?.length ?? 0;
356
+ return {
357
+ value: (before || after) === "-" ? -n : n,
358
+ format: `${symbol}#,##0${decimals ? `.${"0".repeat(decimals)}` : ""}`
359
+ };
360
+ }
361
+ }
362
+ const date = TYPED_DATE.exec(trimmed);
363
+ if (date) {
364
+ const [, y, sep, m, d, h, mi, s] = date;
365
+ const day = `${y}-${pad(m)}-${pad(d)}`;
366
+ const time = h != null ? readTime(h, mi, s) : null;
367
+ if (validDate(day) && (h == null || time)) {
368
+ const dayFormat = `yyyy${sep}${two(m, "m")}${sep}${two(d, "d")}`;
369
+ const canonical = sep === "-" && m.length === 2 && d.length === 2 && (!time || time.canonical);
370
+ const value = time ? `${day} ${time.value}` : day;
371
+ return canonical ? { value } : {
372
+ value,
373
+ format: time ? `${dayFormat} ${time.format}` : dayFormat
374
+ };
375
+ }
376
+ }
377
+ const clock = TYPED_TIME.exec(trimmed);
378
+ if (clock) {
379
+ const time = readTime(clock[1], clock[2], clock[3]);
380
+ if (time) return time.canonical ? { value: time.value } : {
381
+ value: time.value,
382
+ format: time.format
383
+ };
384
+ }
385
+ }
386
+ if (/^true$/i.test(trimmed)) return { value: true };
387
+ if (/^false$/i.test(trimmed)) return { value: false };
388
+ return { value: text };
389
+ }
390
+ function validDate(iso) {
391
+ const date = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
392
+ return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(iso);
393
+ }
394
+ /** The text a cell's editor starts with: what was typed, so editing it changes nothing. */
395
+ function inputText(value) {
396
+ if (value == null) return "";
397
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
398
+ if (typeof value === "number") return generalNumber(value);
399
+ if (value !== "" && !value.startsWith("=") && parseInput(value).value !== value) return `'${value}`;
400
+ return value;
401
+ }
402
+ export { alignOf, changeDecimals, fitNumber, formatValue, generalNumber, inputText, isDateFormat, parseInput, textOf };
403
+
404
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","names":[],"sources":["../../src/core/format.ts"],"sourcesContent":["import type { SheetValue } from './model'\n\n/*\n * Showing values and reading what is typed into a cell.\n *\n * Number formats are a small, predictable subset of the usual pattern\n * language: digits (`0` always shown, `#` only when needed), a thousands\n * comma, `%`, and text before and after (in quotes, after `\\`, or symbols\n * such as `¥`). Up to three sections split by `;` are for positive\n * numbers, negative numbers (shown without their sign) and zero; a section\n * without digits shows its text as it is.\n *\n * Date formats apply to dates written as `YYYY-MM-DD[ HH:mm[:ss]]` and\n * times written as `HH:mm[:ss]`: `yyyy`, `yy`, `m`, `mm` (month, or minutes\n * after an hour or before seconds), `d`, `dd`, `ddd` and `dddd` (the\n * weekday, short and long, in the given locale), `h`, `hh`, `s` and `ss`.\n * Other letters and quoted text show as they are. `@` shows the value as\n * text.\n */\n\ninterface NumberPattern {\n prefix: string\n suffix: string\n /** Digits always shown after the point, and digits shown when needed. */\n fixed: number\n optional: number\n /** Digits always shown before the point. */\n whole: number\n grouping: boolean\n percent: boolean\n}\n\nconst DATE_VALUE = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/\nconst TIME_VALUE = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/\nconst DATE_PART = /^(?:yyyy|yy|dddd|ddd|dd|d|mm|m|hh|h|ss|s)/i\n\nconst patterns = new Map<string, NumberPattern | null>()\n\n/** Splits a section into its digits and the text around them; `null` when it has no digits. */\nfunction parsePattern(section: string): NumberPattern | null {\n let prefix = ''\n let suffix = ''\n let body = ''\n let percent = false\n for (let i = 0; i < section.length; i++) {\n const char = section[i]!\n let text: string | null = null\n if (char === '\"') {\n const close = section.indexOf('\"', i + 1)\n const end = close === -1 ? section.length : close\n text = section.slice(i + 1, end)\n i = end\n } else if (char === '\\\\') text = section[++i] ?? ''\n else if ('0#,.'.includes(char) && !suffix) body += char\n else {\n if (char === '%') percent = true\n text = char\n }\n if (text != null) {\n if (body) suffix += text\n else prefix += text\n }\n }\n if (!/[0#]/.test(body)) return null\n const [whole = '', fraction = ''] = body.split('.')\n return {\n prefix,\n suffix,\n fixed: (fraction.match(/0/g) ?? []).length,\n optional: (fraction.match(/#/g) ?? []).length,\n whole: (whole.match(/0/g) ?? []).length,\n grouping: whole.includes(','),\n percent,\n }\n}\n\nfunction patternOf(section: string) {\n let pattern = patterns.get(section)\n if (pattern === undefined) {\n pattern = parsePattern(section)\n if (patterns.size > 200) patterns.clear()\n patterns.set(section, pattern)\n }\n return pattern\n}\n\nfunction groupDigits(digits: string) {\n let out = ''\n for (let i = 0; i < digits.length; i++) {\n if (i && (digits.length - i) % 3 === 0) out += ','\n out += digits[i]\n }\n return out\n}\n\n/** The text of a section without digits, with quotes and `\\` marks taken off. */\nfunction literalOf(section: string) {\n return section.replace(\n /\"([^\"]*)\"?|\\\\(.)/g,\n (_, quoted?: string, escaped?: string) => quoted ?? escaped ?? '',\n )\n}\n\nfunction formatNumber(value: number, format: string) {\n const sections = format.split(';')\n const zero = value === 0 && sections.length > 2\n const negative = value < 0 && sections.length > 1\n const section = sections[zero ? 2 : negative ? 1 : 0]!\n const pattern = patternOf(section)\n if (!pattern) return zero || negative ? literalOf(section) : null\n const digits = pattern.fixed + pattern.optional\n const n = Math.abs(value) * (pattern.percent ? 100 : 1)\n // rounded in decimal, so 1.005 shows as 1.01 rather than 1.00\n const text = String(n).includes('e')\n ? n.toFixed(digits)\n : Number(`${Math.round(Number(`${n}e${digits}`))}e-${digits}`).toFixed(digits)\n let [whole = '0', fraction = ''] = text.split('.')\n // trailing optional digits only show when they aren't 0\n while (fraction.length > pattern.fixed && fraction.endsWith('0')) fraction = fraction.slice(0, -1)\n whole = whole.replace(/^0+/, '').padStart(pattern.whole, '0')\n if (pattern.grouping) whole = groupDigits(whole)\n const sign = value < 0 && !negative && Number(text) !== 0 ? '-' : ''\n const number = fraction ? `${whole}.${fraction}` : whole || (pattern.fixed ? '0' : '')\n return `${sign}${pattern.prefix}${number}${pattern.suffix}`\n}\n\ntype DateToken = { part: string } | { text: string }\n\nconst dateTokens = new Map<string, DateToken[]>()\n\n/** A date format as parts and the text between them. */\nfunction tokensOf(format: string) {\n let tokens = dateTokens.get(format)\n if (tokens) return tokens\n tokens = []\n for (let i = 0; i < format.length;) {\n const char = format[i]!\n if (char === '\"') {\n const close = format.indexOf('\"', i + 1)\n const end = close === -1 ? format.length : close\n tokens.push({ text: format.slice(i + 1, end) })\n i = end + 1\n continue\n }\n if (char === '\\\\') {\n tokens.push({ text: format[i + 1] ?? '' })\n i += 2\n continue\n }\n const part = DATE_PART.exec(format.slice(i))?.[0]\n if (part) {\n tokens.push({ part: part.toLowerCase() })\n i += part.length\n } else {\n tokens.push({ text: char })\n i++\n }\n }\n // `m` is minutes right after an hour or right before seconds\n const parts = tokens.filter((t): t is { part: string } => 'part' in t)\n parts.forEach((t, i) => {\n if (t.part[0] !== 'm') return\n if (parts[i - 1]?.part[0] === 'h' || parts[i + 1]?.part[0] === 's')\n t.part = t.part === 'mm' ? 'nn' : 'n'\n })\n if (dateTokens.size > 200) dateTokens.clear()\n dateTokens.set(format, tokens)\n return tokens\n}\n\n/** Whether a format has date parts, outside quoted and escaped text. */\nexport const isDateFormat = (format: string) => /[ymdhs]/i.test(format.replace(/\"[^\"]*\"?|\\\\./g, ''))\n\nconst weekdayNames = new Map<string, Intl.DateTimeFormat>()\n\nfunction weekday(y: number, m: number, d: number, style: 'short' | 'long', locale?: string) {\n const key = `${locale ?? ''}|${style}`\n let names = weekdayNames.get(key)\n if (!names) {\n try {\n names = new Intl.DateTimeFormat(locale, { weekday: style, timeZone: 'UTC' })\n } catch {\n names = new Intl.DateTimeFormat(undefined, { weekday: style, timeZone: 'UTC' })\n }\n weekdayNames.set(key, names)\n }\n return names.format(new Date(Date.UTC(y, m - 1, d)))\n}\n\nfunction formatDate(value: string, format: string, locale?: string) {\n let y = ''\n let mo = ''\n let d = ''\n let h: string\n let mi: string\n let s: string\n const date = DATE_VALUE.exec(value)\n if (date) [, y = '', mo = '', d = '', h = '00', mi = '00', s = '00'] = date\n else {\n const time = TIME_VALUE.exec(value)\n if (!time) return null\n ;[, h = '00', mi = '00', s = '00'] = time\n }\n let out = ''\n for (const token of tokensOf(format)) {\n if ('text' in token) {\n out += token.text\n continue\n }\n const { part } = token\n // a time has no date to show\n if (!date && 'ymd'.includes(part[0]!)) return null\n switch (part) {\n case 'yyyy':\n out += y\n break\n case 'yy':\n out += y.slice(2)\n break\n case 'mm':\n out += mo\n break\n case 'm':\n out += Number(mo)\n break\n case 'dddd':\n case 'ddd':\n out += weekday(+y, +mo, +d, part === 'ddd' ? 'short' : 'long', locale)\n break\n case 'dd':\n out += d\n break\n case 'd':\n out += Number(d)\n break\n case 'hh':\n out += h\n break\n case 'h':\n out += Number(h)\n break\n case 'nn':\n out += mi\n break\n case 'n':\n out += Number(mi)\n break\n case 'ss':\n out += s\n break\n default:\n out += Number(s)\n }\n }\n return out\n}\n\n/** Up to 15 significant digits, so 0.1 + 0.2 shows as 0.3. */\nexport function generalNumber(value: number) {\n if (!Number.isFinite(value)) return String(value)\n const rounded = Number(value.toPrecision(15))\n const abs = Math.abs(rounded)\n if (abs !== 0 && (abs >= 1e15 || abs < 1e-9)) return rounded.toExponential().replace('e+', 'E+')\n return String(rounded)\n}\n\n/**\n * A value as the cell shows it, with its format. `locale` names the\n * language for weekdays (`ddd`, `dddd`).\n */\nexport function formatValue(\n value: SheetValue | undefined,\n format?: string,\n locale?: string,\n): string {\n if (value == null) return ''\n if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'\n if (format === '@') return String(value)\n if (typeof value === 'number') {\n if (format && !isDateFormat(format)) {\n const text = formatNumber(value, format)\n if (text != null) return text\n }\n return generalNumber(value)\n }\n if (format && isDateFormat(format)) return formatDate(value, format, locale) ?? value\n return textOf(value)\n}\n\n/** Text as it shows: `'=…` is text starting with `=`, shown without its mark. */\nexport const textOf = (value: string) => (value.startsWith(\"'=\") ? value.slice(1) : value)\n\n/**\n * A number without a format, in at most `chars` characters, for a cell\n * too narrow for all its digits: fewer decimals first, then `1.2E+11`.\n * Only what the cell shows changes, never the value.\n */\nexport function fitNumber(value: number, chars: number) {\n const text = generalNumber(value)\n if (text.length <= chars || !Number.isFinite(value)) return text\n if (!text.includes('E')) {\n const point = text.indexOf('.')\n // the whole part fits: round off decimals\n if (point !== -1 && point <= chars) {\n const decimals = Math.max(0, chars - point - 1)\n const rounded = String(Number(value.toFixed(decimals)))\n if (rounded.length <= chars && rounded !== '0' && rounded !== '-0') return rounded\n }\n }\n let shortest = text\n for (let digits = 14; digits >= 0; digits--) {\n const [mantissa, exponent] = value.toExponential(digits).split('e')\n const out = `${mantissa!.includes('.') ? mantissa!.replace(/\\.?0+$/, '') : mantissa}E${exponent}`\n shortest = out\n if (out.length <= chars) return out\n }\n return shortest\n}\n\n/**\n * A number format with `by` more digits after the point, or fewer when\n * `by` is below 0, as the toolbar's buttons change it: `0.00` → `0.000`,\n * `#,##0` → `#,##0.0`, `0.0%` → `0%`. Without a format it starts from the\n * digits `value` shows. Date, time and text formats stay as they are.\n */\nexport function changeDecimals(\n format: string | undefined,\n value: SheetValue,\n by: number,\n): string | undefined {\n if (!format) {\n const text = typeof value === 'number' ? generalNumber(value) : ''\n const point = text.includes('E') ? -1 : text.indexOf('.')\n const digits = Math.max(0, (point === -1 ? 0 : text.length - point - 1) + by)\n return digits ? `0.${'0'.repeat(digits)}` : '0'\n }\n if (format === '@' || isDateFormat(format)) return format\n return format\n .split(';')\n .map((section) => sectionDecimals(section, by))\n .join(';')\n}\n\n/** One section of a number format with more or fewer digits after the point. */\nfunction sectionDecimals(section: string, by: number) {\n let start = -1\n for (let i = 0; i < section.length; i++) {\n const char = section[i]!\n if (char === '\"') {\n const close = section.indexOf('\"', i + 1)\n i = close === -1 ? section.length : close\n } else if (char === '\\\\') i++\n else if (char === '0' || char === '#') {\n start = i\n break\n }\n }\n if (start === -1) return section\n let end = start\n while (end < section.length && '0#,.'.includes(section[end]!)) end++\n const [whole = '', fraction = ''] = section.slice(start, end).split('.')\n const digits =\n by > 0 ? fraction + '0'.repeat(by) : fraction.slice(0, Math.max(0, fraction.length + by))\n return section.slice(0, start) + (digits ? `${whole}.${digits}` : whole) + section.slice(end)\n}\n\n/**\n * How a value lines up in its cell unless its style says otherwise:\n * numbers, dates and times to the right, `TRUE` and `FALSE` in the middle,\n * text to the left.\n */\nexport function alignOf(value: SheetValue | undefined): 'left' | 'center' | 'right' {\n if (typeof value === 'number') return 'right'\n if (typeof value === 'boolean') return 'center'\n if (typeof value === 'string' && (DATE_VALUE.test(value) || TIME_VALUE.test(value)))\n return 'right'\n return 'left'\n}\n\nconst NUMBER = /^[-+]?(?:\\d{1,3}(?:,\\d{3})+|\\d+)?(?:\\.\\d+)?(?:e[-+]?\\d+)?$/i\nconst PLAIN_NUMBER = /^(?:\\d{1,3}(?:,\\d{3})+|\\d+)(?:\\.(\\d+))?$/\nconst TYPED_DATE = /^(\\d{4})([-/.])(\\d{1,2})\\2(\\d{1,2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$/\nconst TYPED_TIME = /^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/\nconst CURRENCY = /^([-+]?)([¥$€£])\\s?([-+]?)(.+)$/\n\n/** What typing `text` into a cell writes: the value, and a format when the text implies one. */\nexport interface SheetInput {\n value: SheetValue\n format?: string\n}\n\nconst pad = (n: string) => n.padStart(2, '0')\n\n/** A date part's format letter, doubled when it was typed with a leading zero. */\nconst two = (part: string, letter: string) => (part.startsWith('0') ? letter + letter : letter)\n\n/** `HH:mm[:ss]` and the format that shows it as typed, or `null` when it isn't a time. */\nfunction readTime(h: string, m: string, s?: string) {\n if (Number(h) > 23 || Number(m) > 59 || (s != null && Number(s) > 59)) return null\n const value = `${pad(h)}:${m}${s != null ? `:${s}` : ''}`\n // `hh` only when the hour was typed with its zero: `9:30` and `12:30` are `h:mm`\n const format = `${h.startsWith('0') ? 'hh' : 'h'}:mm${s != null ? ':ss' : ''}`\n return { value, format, canonical: h.length === 2 }\n}\n\n/**\n * Reads typed text: numbers (with thousands commas, `12%`, `¥1,200.50`),\n * `TRUE` and `FALSE`, dates (`2026-10-31`, `2026/3/5`, `2026.3.5`, with a\n * time or not), times (`9:30`), formulas (`=…`) and text. A leading `'`\n * keeps the rest as text. Dates and times are written the standard way\n * (`2026-03-05`, `09:30`); when typed another way, the format shows them\n * as typed.\n */\nexport function parseInput(text: string): SheetInput {\n if (text === '') return { value: null }\n // text that starts with `=` keeps its mark, so it isn't a formula\n if (text.startsWith(\"'\")) return { value: text.startsWith(\"'=\") ? text : text.slice(1) }\n if (text.startsWith('=') && text.length > 1) return { value: text }\n const trimmed = text.trim()\n if (/^\\S/.test(trimmed) && trimmed !== '.' && /\\d/.test(trimmed)) {\n if (NUMBER.test(trimmed)) return { value: Number(trimmed.replaceAll(',', '')) }\n if (trimmed.endsWith('%') && NUMBER.test(trimmed.slice(0, -1))) {\n const n = Number(trimmed.slice(0, -1).replaceAll(',', '')) / 100\n const decimals = /\\.(\\d+)%$/.exec(trimmed)?.[1]!.length ?? 0\n const format = decimals ? `0.${'0'.repeat(decimals)}%` : '0%'\n return { value: Number(n.toPrecision(15)), format }\n }\n const money = CURRENCY.exec(trimmed)\n if (money && !(money[1] && money[3])) {\n const [, before, symbol, after, digits] = money\n const number = PLAIN_NUMBER.exec(digits!)\n if (number) {\n const n = Number(digits!.replaceAll(',', ''))\n const decimals = number[1]?.length ?? 0\n return {\n value: (before || after) === '-' ? -n : n,\n format: `${symbol}#,##0${decimals ? `.${'0'.repeat(decimals)}` : ''}`,\n }\n }\n }\n const date = TYPED_DATE.exec(trimmed)\n if (date) {\n const [, y, sep, m, d, h, mi, s] = date\n const day = `${y}-${pad(m!)}-${pad(d!)}`\n const time = h != null ? readTime(h, mi!, s) : null\n if (validDate(day) && (h == null || time)) {\n // two digits only where they were typed with a zero: `2026/12/5` is `yyyy/m/d`\n const dayFormat = `yyyy${sep}${two(m!, 'm')}${sep}${two(d!, 'd')}`\n const canonical =\n sep === '-' && m!.length === 2 && d!.length === 2 && (!time || time.canonical)\n const value = time ? `${day} ${time.value}` : day\n return canonical\n ? { value }\n : { value, format: time ? `${dayFormat} ${time.format}` : dayFormat }\n }\n }\n const clock = TYPED_TIME.exec(trimmed)\n if (clock) {\n const time = readTime(clock[1]!, clock[2]!, clock[3])\n if (time)\n return time.canonical ? { value: time.value } : { value: time.value, format: time.format }\n }\n }\n if (/^true$/i.test(trimmed)) return { value: true }\n if (/^false$/i.test(trimmed)) return { value: false }\n return { value: text }\n}\n\nfunction validDate(iso: string) {\n const date = new Date(`${iso}T00:00:00Z`)\n return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(iso)\n}\n\n/** The text a cell's editor starts with: what was typed, so editing it changes nothing. */\nexport function inputText(value: SheetValue | undefined): string {\n if (value == null) return ''\n if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'\n if (typeof value === 'number') return generalNumber(value)\n // text that would read as something else keeps its mark\n if (value !== '' && !value.startsWith('=') && parseInput(value).value !== value)\n return `'${value}`\n return value\n}\n"],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ import { boundsOf, columnName, parseRange } from "./address.js";
2
+ /** The lines a group's range covers, or `null` when it isn't whole rows or columns. */
3
+ function groupSpan(range) {
4
+ const parsed = parseRange(range);
5
+ if (!parsed) return null;
6
+ const b = boundsOf(parsed);
7
+ if (b.left === 0 && b.right === Infinity && b.bottom !== Infinity) return {
8
+ axis: "row",
9
+ start: b.top,
10
+ end: b.bottom
11
+ };
12
+ if (b.top === 0 && b.bottom === Infinity && b.right !== Infinity) return {
13
+ axis: "col",
14
+ start: b.left,
15
+ end: b.right
16
+ };
17
+ return null;
18
+ }
19
+ /** A group's range for its lines: `'3:8'` or `'B:D'`. */
20
+ var groupRange = (axis, start, end) => axis === "row" ? `${start + 1}:${end + 1}` : `${columnName(start)}:${columnName(end)}`;
21
+ /** One axis of a sheet's groups. */
22
+ var SheetGroupAxis = class {
23
+ list;
24
+ dropped;
25
+ /** Lines inside a folded group. */
26
+ hidden = /* @__PURE__ */ new Set();
27
+ /** The deepest a group nests; `0` without groups. */
28
+ depth;
29
+ constructor(list, dropped) {
30
+ this.list = list;
31
+ this.dropped = dropped;
32
+ let depth = 0;
33
+ for (const g of list) {
34
+ depth = Math.max(depth, g.depth);
35
+ if (g.collapsed) for (let i = g.start; i <= g.end; i++) this.hidden.add(i);
36
+ }
37
+ this.depth = depth;
38
+ }
39
+ /** The groups around a line, outer first. */
40
+ around(line) {
41
+ return this.list.filter((g) => g.start <= line && g.end >= line);
42
+ }
43
+ /** The group with exactly these lines. */
44
+ find(start, end) {
45
+ return this.list.find((g) => g.start === start && g.end === end);
46
+ }
47
+ /**
48
+ * The line a group's button sits on: its summary line, before or after
49
+ * it; the other side when that line is missing or inside a group beside
50
+ * it; `-1` when neither side will do.
51
+ */
52
+ buttonLine(g, count, summary = "before") {
53
+ const sides = summary === "after" ? [g.end + 1, g.start - 1] : [g.start - 1, g.end + 1];
54
+ for (const line of sides) if (line >= 0 && line < count && !this.list.some((o) => o !== g && o.depth >= g.depth && o.start <= line && o.end >= line)) return line;
55
+ return -1;
56
+ }
57
+ /**
58
+ * Why lines can't be grouped: a group they would cross, or `'deep'` for
59
+ * nesting more than eight deep; `null` when they can.
60
+ */
61
+ problem(start, end) {
62
+ let around = 0;
63
+ let inside = 0;
64
+ for (const g of this.list) {
65
+ if (g.start < start && g.end >= start && g.end < end || g.start > start && g.start <= end && g.end > end) return { cross: g };
66
+ if (g.start <= start && g.end >= end) around++;
67
+ else if (g.start >= start && g.end <= end) inside = Math.max(inside, g.depth - around);
68
+ }
69
+ return around + 1 + inside > 8 ? "deep" : null;
70
+ }
71
+ /** The groups at a depth that sit right beside these lines, inside the same parent. */
72
+ beside(start, end) {
73
+ const parent = this.list.filter((g) => g.start <= start && g.end >= end).at(-1);
74
+ const depth = (parent?.depth ?? 0) + 1;
75
+ return this.list.filter((g) => g.depth === depth && (g.end === start - 1 || g.start === end + 1) && (!parent || g.start >= parent.start && g.end <= parent.end));
76
+ }
77
+ };
78
+ /** The groups of a sheet, laid out per axis. */
79
+ var SheetGroups = class SheetGroups {
80
+ static EMPTY = new SheetGroups(void 0);
81
+ rows;
82
+ cols;
83
+ constructor(groups, collapsed) {
84
+ const rows = [];
85
+ const cols = [];
86
+ groups?.forEach((group, index) => {
87
+ const span = groupSpan(group.range);
88
+ if (!span) return;
89
+ const folded = collapsed ? collapsed(group, index) : !!group.collapsed;
90
+ (span.axis === "row" ? rows : cols).push({
91
+ ...span,
92
+ index,
93
+ depth: 0,
94
+ collapsed: folded
95
+ });
96
+ });
97
+ this.rows = layOut(rows);
98
+ this.cols = layOut(cols);
99
+ }
100
+ axis(axis) {
101
+ return axis === "row" ? this.rows : this.cols;
102
+ }
103
+ };
104
+ /** Gives each group its depth, leaving out those that cross another or nest too deep. */
105
+ function layOut(list) {
106
+ list.sort((a, b) => a.start - b.start || b.end - a.end || a.index - b.index);
107
+ const kept = [];
108
+ const open = [];
109
+ let dropped = 0;
110
+ for (const g of list) {
111
+ while (open.length && open.at(-1).end < g.start) open.pop();
112
+ const parent = open.at(-1);
113
+ if (parent && (g.end > parent.end || g.start === parent.start && g.end === parent.end) || open.length >= 8) {
114
+ dropped++;
115
+ continue;
116
+ }
117
+ g.depth = open.length + 1;
118
+ open.push(g);
119
+ kept.push(g);
120
+ }
121
+ return new SheetGroupAxis(kept, dropped);
122
+ }
123
+ var cache = /* @__PURE__ */ new WeakMap();
124
+ /** A sheet's groups, read once per list. */
125
+ function groupsOf(sheet) {
126
+ const list = sheet.groups;
127
+ if (!list?.length) return SheetGroups.EMPTY;
128
+ let groups = cache.get(list);
129
+ if (!groups) cache.set(list, groups = new SheetGroups(list));
130
+ return groups;
131
+ }
132
+ export { SheetGroupAxis, SheetGroups, groupRange, groupSpan, groupsOf };
133
+
134
+ //# sourceMappingURL=group.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group.js","names":[],"sources":["../../src/core/group.ts"],"sourcesContent":["import { boundsOf, columnName, parseRange } from './address'\nimport type { SheetData, SheetGroup } from './model'\n\n/*\n * Groups of rows or columns that fold away together. A sheet lists them as\n * whole rows (`'3:8'`) or whole columns (`'B:D'`); groups on the same axis\n * nest or stay apart, at most eight deep. A group's button sits on the line\n * that holds its heading or subtotal: the one before it, or after it when\n * the sheet's `groupSummary` says so.\n */\n\n/** How deep groups may nest. */\nexport const MAX_GROUP_DEPTH = 8\n\n/** How wide each lane of the outline beside the headers is. */\nexport const OUTLINE_LANE = 16\n\ntype Axis = 'row' | 'col'\n\n/** A group of a sheet, laid out. */\nexport interface SheetGroupLines {\n /** Its place in the sheet's `groups`. */\n index: number\n axis: Axis\n /** Its first and last line. */\n start: number\n end: number\n /** 1 for a group inside no other. */\n depth: number\n collapsed: boolean\n}\n\n/** The lines a group's range covers, or `null` when it isn't whole rows or columns. */\nexport function groupSpan(range: string): { axis: Axis; start: number; end: number } | null {\n const parsed = parseRange(range)\n if (!parsed) return null\n const b = boundsOf(parsed)\n if (b.left === 0 && b.right === Infinity && b.bottom !== Infinity)\n return { axis: 'row', start: b.top, end: b.bottom }\n if (b.top === 0 && b.bottom === Infinity && b.right !== Infinity)\n return { axis: 'col', start: b.left, end: b.right }\n return null\n}\n\n/** A group's range for its lines: `'3:8'` or `'B:D'`. */\nexport const groupRange = (axis: Axis, start: number, end: number) =>\n axis === 'row' ? `${start + 1}:${end + 1}` : `${columnName(start)}:${columnName(end)}`\n\n/** One axis of a sheet's groups. */\nexport class SheetGroupAxis {\n /** Lines inside a folded group. */\n readonly hidden = new Set<number>()\n /** The deepest a group nests; `0` without groups. */\n readonly depth: number\n\n constructor(\n /** Outer groups before the ones inside them. */\n readonly list: readonly SheetGroupLines[],\n /** Groups left out for crossing another or nesting too deep. */\n readonly dropped: number,\n ) {\n let depth = 0\n for (const g of list) {\n depth = Math.max(depth, g.depth)\n if (g.collapsed) for (let i = g.start; i <= g.end; i++) this.hidden.add(i)\n }\n this.depth = depth\n }\n\n /** The groups around a line, outer first. */\n around(line: number) {\n return this.list.filter((g) => g.start <= line && g.end >= line)\n }\n\n /** The group with exactly these lines. */\n find(start: number, end: number) {\n return this.list.find((g) => g.start === start && g.end === end)\n }\n\n /**\n * The line a group's button sits on: its summary line, before or after\n * it; the other side when that line is missing or inside a group beside\n * it; `-1` when neither side will do.\n */\n buttonLine(g: SheetGroupLines, count: number, summary: SheetGroupSummary = 'before') {\n const sides = summary === 'after' ? [g.end + 1, g.start - 1] : [g.start - 1, g.end + 1]\n for (const line of sides)\n if (\n line >= 0 &&\n line < count &&\n !this.list.some((o) => o !== g && o.depth >= g.depth && o.start <= line && o.end >= line)\n )\n return line\n return -1\n }\n\n /**\n * Why lines can't be grouped: a group they would cross, or `'deep'` for\n * nesting more than eight deep; `null` when they can.\n */\n problem(start: number, end: number): { cross: SheetGroupLines } | 'deep' | null {\n let around = 0\n let inside = 0\n for (const g of this.list) {\n const crosses =\n (g.start < start && g.end >= start && g.end < end) ||\n (g.start > start && g.start <= end && g.end > end)\n if (crosses) return { cross: g }\n if (g.start <= start && g.end >= end) around++\n else if (g.start >= start && g.end <= end) inside = Math.max(inside, g.depth - around)\n }\n return around + 1 + inside > MAX_GROUP_DEPTH ? 'deep' : null\n }\n\n /** The groups at a depth that sit right beside these lines, inside the same parent. */\n beside(start: number, end: number) {\n const parent = this.list.filter((g) => g.start <= start && g.end >= end).at(-1)\n const depth = (parent?.depth ?? 0) + 1\n return this.list.filter(\n (g) =>\n g.depth === depth &&\n (g.end === start - 1 || g.start === end + 1) &&\n (!parent || (g.start >= parent.start && g.end <= parent.end)),\n )\n }\n}\n\n/** Which side of a group holds its heading or subtotal, and its button. */\nexport type SheetGroupSummary = 'before' | 'after'\n\n/** The groups of a sheet, laid out per axis. */\nexport class SheetGroups {\n static readonly EMPTY = new SheetGroups(undefined)\n\n readonly rows: SheetGroupAxis\n readonly cols: SheetGroupAxis\n\n constructor(\n groups: readonly SheetGroup[] | undefined,\n /** Whether a group is folded, when it isn't what the sheet says. */\n collapsed?: (group: SheetGroup, index: number) => boolean,\n ) {\n const rows: SheetGroupLines[] = []\n const cols: SheetGroupLines[] = []\n groups?.forEach((group, index) => {\n const span = groupSpan(group.range)\n if (!span) return\n const folded = collapsed ? collapsed(group, index) : !!group.collapsed\n ;(span.axis === 'row' ? rows : cols).push({ ...span, index, depth: 0, collapsed: folded })\n })\n this.rows = layOut(rows)\n this.cols = layOut(cols)\n }\n\n axis(axis: Axis) {\n return axis === 'row' ? this.rows : this.cols\n }\n}\n\n/** Gives each group its depth, leaving out those that cross another or nest too deep. */\nfunction layOut(list: SheetGroupLines[]) {\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts the list built for it\n list.sort((a, b) => a.start - b.start || b.end - a.end || a.index - b.index)\n const kept: SheetGroupLines[] = []\n const open: SheetGroupLines[] = []\n let dropped = 0\n for (const g of list) {\n while (open.length && open.at(-1)!.end < g.start) open.pop()\n const parent = open.at(-1)\n if (\n (parent && (g.end > parent.end || (g.start === parent.start && g.end === parent.end))) ||\n open.length >= MAX_GROUP_DEPTH\n ) {\n dropped++\n continue\n }\n g.depth = open.length + 1\n open.push(g)\n kept.push(g)\n }\n return new SheetGroupAxis(kept, dropped)\n}\n\nconst cache = new WeakMap<readonly SheetGroup[], SheetGroups>()\n\n/** A sheet's groups, read once per list. */\nexport function groupsOf(sheet: SheetData): SheetGroups {\n const list = sheet.groups\n if (!list?.length) return SheetGroups.EMPTY\n let groups = cache.get(list)\n if (!groups) cache.set(list, (groups = new SheetGroups(list)))\n return groups\n}\n"],"mappings":"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"}