@rowsncolumns/utils 9.5.6 → 9.6.1

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.
@@ -60,12 +60,19 @@ export const getExtendedValueError = (extValue) => {
60
60
  * state hooks' row auto-height so both agree on which cells wrap.
61
61
  */
62
62
  export const alignmentImpliesWrap = (align) => align === "justify" || align === "distributed";
63
+ /**
64
+ * Excel's vertical `justify` and `distributed` wrap the text too: the lines
65
+ * are spread over the cell height, so there must be lines to spread.
66
+ */
67
+ export const verticalAlignmentImpliesWrap = (align) => align === "justify" || align === "distributed";
63
68
  /**
64
69
  * Whether a cell format wraps its text — explicit `wrapStrategy: "wrap"` or
65
- * an alignment that wraps implicitly ({@link alignmentImpliesWrap}).
70
+ * an alignment that wraps implicitly ({@link alignmentImpliesWrap},
71
+ * {@link verticalAlignmentImpliesWrap}).
66
72
  */
67
73
  export const cellFormatWrapsText = (format) => format?.wrapStrategy === "wrap" ||
68
- alignmentImpliesWrap(format?.horizontalAlignment);
74
+ alignmentImpliesWrap(format?.horizontalAlignment) ||
75
+ verticalAlignmentImpliesWrap(format?.verticalAlignment);
69
76
  export const resolveDefaultHorizontalAlignment = ({ errorValue, userEnteredValue, effectiveValue, dataValidation, }) => {
70
77
  const isBoolean = getExtendedValueBool(userEnteredValue) !== undefined;
71
78
  const isCheckbox = dataValidation?.condition?.type === "BOOLEAN";
@@ -0,0 +1,11 @@
1
+ export type CjkLang = "ja" | "zh-Hans" | "zh-Hant" | "ko";
2
+ export interface CjkPreparedFormat {
3
+ /** The rewritten code for the base formatter. */
4
+ code: string;
5
+ /** Converts the rendered text's digits into the `[DBNumN]` numeral system, if any. */
6
+ finish: (out: string) => string;
7
+ }
8
+ export declare const prepareCjkNumberFormat: (code: string, value: number, workbookLocale?: string) => CjkPreparedFormat | undefined;
9
+ /** Language of an LCID (hex, low 16 bits) or a BCP-47 tag, for the CJK locales. */
10
+ export declare const cjkLangOfTag: (input: string) => CjkLang | undefined;
11
+ //# sourceMappingURL=cjk-number-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cjk-number-format.d.ts","sourceRoot":"","sources":["../../cjk-number-format.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;AAS1D,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACjC;AAgBD,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,EACZ,OAAO,MAAM,EACb,iBAAiB,MAAM,KACtB,iBAAiB,GAAG,SAsEtB,CAAC;AAqLF,mFAAmF;AACnF,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,OAAO,GAAG,SA8BtD,CAAC"}
@@ -0,0 +1,494 @@
1
+ /**
2
+ * East Asian number-format tokens `numfmt` does not render: the `[DBNum1]`–`[DBNum4]` numeral
3
+ * systems, the Japanese emperor-era tokens (`g`/`gg`/`ggg`, and `e`/`ee` under a Japanese locale)
4
+ * and the `aaa`/`aaaa` weekday tokens. TypeScript twin of the engine's Rust
5
+ * `rnc-cellmodel/src/cjk_format.rs` — keep the two in step.
6
+ *
7
+ * {@link prepareCjkNumberFormat} rewrites a format code so the base formatter can render it (era
8
+ * names, era years and weekday names become quoted literals computed from the date serial,
9
+ * `[DBNumN]` is removed, and under `[DBNumN]` a date's year becomes a quoted literal in that numeral
10
+ * system); `finish` then turns the remaining ASCII digits into the numeral system:
11
+ *
12
+ * - `General` and date fields read as a number with units (`123` → `百二十三`, `15日` → `十五日`);
13
+ * - digit placeholders (`0`, `#`) and years read digit by digit (`[DBNum1]0` → `一二三`).
14
+ *
15
+ * The language comes from the code's `[$-LCID]` / `[$-tag]` token, then the workbook locale, and
16
+ * defaults to Japanese.
17
+ */
18
+ import { analyzeFormatSection, pickNumberSection, splitFormatSections, } from "./excel-number-format";
19
+ const DBNUM_TOKEN = /^\[dbnum([1-4])\]$/i;
20
+ /** Cheap pre-filter: a `DBNum` token, a `g` (era name or `General`), `aaa`, or an `e` beside a locale token. */
21
+ const mightContainTokens = (code) => /[gG]|dbnum|aaa/i.test(code) || (/[eE]/.test(code) && code.includes("[$"));
22
+ export const prepareCjkNumberFormat = (code, value, workbookLocale) => {
23
+ if (!mightContainTokens(code))
24
+ return undefined;
25
+ const tag = localeTag(code);
26
+ const tagLang = tag ? cjkLangOfTag(tag) : undefined;
27
+ const lang = tagLang ??
28
+ (workbookLocale ? cjkLangOfTag(workbookLocale) : undefined) ??
29
+ "ja";
30
+ const gannen = !!tag && tag.toLowerCase().includes("-x-gannen");
31
+ const date = serialToDate(value);
32
+ const sections = splitFormatSections(code);
33
+ const rewritten = [];
34
+ const levels = [];
35
+ const dated = [];
36
+ let changed = false;
37
+ for (const section of sections) {
38
+ const level = dbnumLevel(section);
39
+ const result = rewriteSection(section, {
40
+ dbnum: level === undefined ? undefined : { level, lang },
41
+ weekdayLang: lang,
42
+ eraYears: tagLang === "ja" || hasEraNameToken(section),
43
+ gannen,
44
+ date,
45
+ });
46
+ changed ||= result.changed || level !== undefined;
47
+ rewritten.push(result.text);
48
+ levels.push(level);
49
+ dated.push(result.dateToken);
50
+ }
51
+ if (!changed)
52
+ return undefined;
53
+ const anyCondition = sections.some((s) => analyzeFormatSection(s).hasCondition);
54
+ const index = anyCondition
55
+ ? 0
56
+ : Math.min(pickNumberSection(sections.length, value).index, sections.length - 1);
57
+ const level = levels[index];
58
+ const dbnum = level !== undefined && (level <= 3 || (level === 4 && lang === "ko"))
59
+ ? { level, lang }
60
+ : undefined;
61
+ let mode;
62
+ if (dated[index]) {
63
+ mode = "date";
64
+ }
65
+ else {
66
+ switch (analyzeFormatSection(stripDbNum(sections[index])).kind) {
67
+ case "general":
68
+ mode = "general";
69
+ break;
70
+ case "date":
71
+ mode = "date";
72
+ break;
73
+ case "number":
74
+ case "other":
75
+ mode = "digits";
76
+ break;
77
+ default:
78
+ mode = "unchanged";
79
+ }
80
+ }
81
+ return {
82
+ code: rewritten.join(";"),
83
+ finish: (out) => dbnum && mode !== "unchanged" ? convertDigits(out, dbnum, mode) : out,
84
+ };
85
+ };
86
+ const runLength = (chars, from, letter) => {
87
+ let n = 0;
88
+ while (from + n < chars.length && chars[from + n].toLowerCase() === letter)
89
+ n += 1;
90
+ return n;
91
+ };
92
+ const startsWithCi = (chars, from, word) => chars
93
+ .slice(from, from + word.length)
94
+ .join("")
95
+ .toLowerCase() === word;
96
+ const quote = (text) => `"${text}"`;
97
+ const rewriteSection = (section, ctx) => {
98
+ const chars = Array.from(section);
99
+ let out = "";
100
+ let changed = false;
101
+ let dateToken = false;
102
+ let i = 0;
103
+ while (i < chars.length) {
104
+ const c = chars[i];
105
+ const lower = c.toLowerCase();
106
+ if (c === '"') {
107
+ const close = chars.indexOf('"', i + 1);
108
+ const end = close === -1 ? chars.length : close + 1;
109
+ out += chars.slice(i, end).join("");
110
+ i = end;
111
+ }
112
+ else if (c === "\\" || c === "_" || c === "*") {
113
+ out += chars.slice(i, i + 2).join("");
114
+ i += 2;
115
+ }
116
+ else if (c === "[") {
117
+ const close = chars.indexOf("]", i);
118
+ const end = close === -1 ? chars.length : close + 1;
119
+ const token = chars.slice(i, end).join("");
120
+ if (DBNUM_TOKEN.test(token))
121
+ changed = true;
122
+ else
123
+ out += token;
124
+ i = end;
125
+ }
126
+ else if (lower === "g" && startsWithCi(chars, i, "general")) {
127
+ out += chars.slice(i, i + 7).join("");
128
+ i += 7;
129
+ }
130
+ else if (lower === "g") {
131
+ const run = runLength(chars, i, "g");
132
+ if (ctx.date) {
133
+ const era = eraOf(ctx.date);
134
+ out += quote(run === 1 ? era.letter : run === 2 ? era.short : era.name);
135
+ }
136
+ changed = true;
137
+ dateToken = true;
138
+ i += run;
139
+ }
140
+ else if (lower === "e" && chars[i + 1] !== "+" && chars[i + 1] !== "-") {
141
+ const run = runLength(chars, i, "e");
142
+ if (ctx.date && ctx.eraYears) {
143
+ const eraYear = ctx.date.year - eraOf(ctx.date).start[0] + 1;
144
+ const text = eraYear === 1 && ctx.gannen
145
+ ? "元"
146
+ : run >= 2
147
+ ? String(eraYear).padStart(2, "0")
148
+ : String(eraYear);
149
+ out += quote(text);
150
+ changed = true;
151
+ dateToken = true;
152
+ }
153
+ else if (ctx.date && ctx.dbnum) {
154
+ out += quote(perDigit(String(ctx.date.year), ctx.dbnum));
155
+ changed = true;
156
+ dateToken = true;
157
+ }
158
+ else {
159
+ out += chars.slice(i, i + run).join("");
160
+ }
161
+ i += run;
162
+ }
163
+ else if (lower === "a" && startsWithCi(chars, i, "am/pm")) {
164
+ out += chars.slice(i, i + 5).join("");
165
+ i += 5;
166
+ }
167
+ else if (lower === "a" && startsWithCi(chars, i, "a/p")) {
168
+ out += chars.slice(i, i + 3).join("");
169
+ i += 3;
170
+ }
171
+ else if (lower === "a" && runLength(chars, i, "a") >= 3) {
172
+ const run = runLength(chars, i, "a");
173
+ if (ctx.date)
174
+ out += quote(weekdayName(ctx.weekdayLang, ctx.date.weekday, run >= 4));
175
+ changed = true;
176
+ dateToken = true;
177
+ i += run;
178
+ }
179
+ else if (lower === "y" && ctx.dbnum && ctx.date) {
180
+ const run = runLength(chars, i, "y");
181
+ const digits = run <= 2
182
+ ? String(ctx.date.year % 100).padStart(2, "0")
183
+ : String(ctx.date.year);
184
+ out += quote(perDigit(digits, ctx.dbnum));
185
+ changed = true;
186
+ dateToken = true;
187
+ i += run;
188
+ }
189
+ else {
190
+ out += c;
191
+ i += 1;
192
+ }
193
+ }
194
+ return { text: out, changed, dateToken };
195
+ };
196
+ /** `[...]` tokens outside quoted literals and escapes. */
197
+ const bracketTokens = (section) => {
198
+ const tokens = [];
199
+ let inQuotes = false;
200
+ for (let i = 0; i < section.length; i += 1) {
201
+ const c = section[i];
202
+ if (c === '"')
203
+ inQuotes = !inQuotes;
204
+ else if (!inQuotes && (c === "\\" || c === "_" || c === "*"))
205
+ i += 1;
206
+ else if (!inQuotes && c === "[") {
207
+ const end = section.indexOf("]", i);
208
+ if (end !== -1) {
209
+ tokens.push(section.slice(i, end + 1));
210
+ i = end;
211
+ }
212
+ }
213
+ }
214
+ return tokens;
215
+ };
216
+ const dbnumLevel = (section) => {
217
+ for (const token of bracketTokens(section)) {
218
+ const m = DBNUM_TOKEN.exec(token);
219
+ if (m)
220
+ return Number(m[1]);
221
+ }
222
+ return undefined;
223
+ };
224
+ const stripDbNum = (section) => bracketTokens(section)
225
+ .filter((t) => DBNUM_TOKEN.test(t))
226
+ .reduce((s, t) => s.replace(t, ""), section);
227
+ /** Whether a section names an era (`g`) outside quotes, brackets and `General`. */
228
+ const hasEraNameToken = (section) => {
229
+ let inQuotes = false;
230
+ let inBrackets = false;
231
+ for (let i = 0; i < section.length; i += 1) {
232
+ const c = section[i];
233
+ if (inQuotes)
234
+ inQuotes = c !== '"';
235
+ else if (inBrackets)
236
+ inBrackets = c !== "]";
237
+ else if (c === '"')
238
+ inQuotes = true;
239
+ else if (c === "[")
240
+ inBrackets = true;
241
+ else if (c === "\\" || c === "_" || c === "*")
242
+ i += 1;
243
+ else if (c.toLowerCase() === "g") {
244
+ if (section.slice(i, i + 7).toLowerCase() === "general")
245
+ i += 6;
246
+ else
247
+ return true;
248
+ }
249
+ }
250
+ return false;
251
+ };
252
+ /** The locale spec of the code's first `[$…-spec]` token (`411`, `ja-JP`, `ja-JP-x-gannen`). */
253
+ const localeTag = (code) => {
254
+ const start = code.indexOf("[$");
255
+ if (start === -1)
256
+ return undefined;
257
+ const end = code.indexOf("]", start);
258
+ if (end === -1)
259
+ return undefined;
260
+ const inner = code.slice(start + 2, end);
261
+ const dash = inner.indexOf("-");
262
+ if (dash === -1)
263
+ return undefined;
264
+ const spec = inner.slice(dash + 1);
265
+ return spec || undefined;
266
+ };
267
+ /** Language of an LCID (hex, low 16 bits) or a BCP-47 tag, for the CJK locales. */
268
+ export const cjkLangOfTag = (input) => {
269
+ const spec = input.split(",")[0].trim();
270
+ if (/^[0-9a-f]+$/i.test(spec)) {
271
+ switch (parseInt(spec, 16) & 0xffff) {
272
+ case 0x0411:
273
+ return "ja";
274
+ case 0x0804:
275
+ case 0x1004:
276
+ return "zh-Hans";
277
+ case 0x0404:
278
+ case 0x0c04:
279
+ case 0x1404:
280
+ return "zh-Hant";
281
+ case 0x0412:
282
+ return "ko";
283
+ default:
284
+ return undefined;
285
+ }
286
+ }
287
+ const [primary, ...rest] = spec.toLowerCase().split(/[-_]/);
288
+ if (primary === "ja")
289
+ return "ja";
290
+ if (primary === "ko")
291
+ return "ko";
292
+ if (primary === "zh") {
293
+ return rest.some((p) => p === "hant" || p === "tw" || p === "hk" || p === "mo")
294
+ ? "zh-Hant"
295
+ : "zh-Hans";
296
+ }
297
+ return undefined;
298
+ };
299
+ // ---- dates ------------------------------------------------------------------------------------
300
+ /**
301
+ * Excel 1900-system serial → calendar date, keeping Excel's fictitious 1900-02-29 (serial 60)
302
+ * and its weekday numbering (serial 1 is a Sunday). The time of day rounds to the nearest second.
303
+ */
304
+ const serialToDate = (value) => {
305
+ if (!Number.isFinite(value) || value < 0 || value >= 2958466)
306
+ return undefined;
307
+ const days = Math.floor(Math.round(value * 86400) / 86400);
308
+ const weekday = (days + 6) % 7;
309
+ if (days === 60)
310
+ return { year: 1900, month: 2, day: 29, weekday };
311
+ const offset = days < 60 ? days : days - 1;
312
+ const d = new Date(Date.UTC(1899, 11, 31) + offset * 86400000);
313
+ return {
314
+ year: d.getUTCFullYear(),
315
+ month: d.getUTCMonth() + 1,
316
+ day: d.getUTCDate(),
317
+ weekday,
318
+ };
319
+ };
320
+ /** Japanese eras with the start dates Windows' Japanese calendar (and so Excel) uses. */
321
+ const ERAS = [
322
+ { start: [1868, 1, 1], letter: "M", short: "明", name: "明治" },
323
+ { start: [1912, 7, 30], letter: "T", short: "大", name: "大正" },
324
+ { start: [1926, 12, 25], letter: "S", short: "昭", name: "昭和" },
325
+ { start: [1989, 1, 8], letter: "H", short: "平", name: "平成" },
326
+ { start: [2019, 5, 1], letter: "R", short: "令", name: "令和" },
327
+ ];
328
+ const eraOf = (date) => {
329
+ const key = date.year * 10000 + date.month * 100 + date.day;
330
+ for (let i = ERAS.length - 1; i >= 0; i -= 1) {
331
+ const [y, m, d] = ERAS[i].start;
332
+ if (key >= y * 10000 + m * 100 + d)
333
+ return ERAS[i];
334
+ }
335
+ return ERAS[0];
336
+ };
337
+ const weekdayName = (lang, weekday, long) => {
338
+ const w = weekday % 7;
339
+ switch (lang) {
340
+ case "ja": {
341
+ const short = "日月火水木金土"[w];
342
+ return long ? `${short}曜日` : short;
343
+ }
344
+ case "ko": {
345
+ const short = "일월화수목금토"[w];
346
+ return long ? `${short}요일` : short;
347
+ }
348
+ default: {
349
+ const short = "日一二三四五六"[w];
350
+ return long ? `星期${short}` : short;
351
+ }
352
+ }
353
+ };
354
+ const KANJI = Array.from("〇一二三四五六七八九");
355
+ const FULL_WIDTH = Array.from("0123456789");
356
+ const BIG_HANS = ["万", "亿", "兆"];
357
+ const BIG_TRAD = ["萬", "億", "兆"];
358
+ const BIG_JA = ["万", "億", "兆"];
359
+ const numerals = ({ level, lang }) => {
360
+ const zh = lang === "zh-Hans" || lang === "zh-Hant";
361
+ const level1Big = lang === "ja" ? BIG_JA : lang === "zh-Hans" ? BIG_HANS : BIG_TRAD;
362
+ if (level === 2) {
363
+ switch (lang) {
364
+ case "ja":
365
+ return {
366
+ digits: Array.from("〇壱弐参四伍六七八九"),
367
+ small: ["拾", "百", "阡"],
368
+ big: BIG_TRAD,
369
+ omitOne: false,
370
+ fillZero: false,
371
+ };
372
+ case "zh-Hans":
373
+ return {
374
+ digits: Array.from("零壹贰叁肆伍陆柒捌玖"),
375
+ small: ["拾", "佰", "仟"],
376
+ big: BIG_HANS,
377
+ omitOne: false,
378
+ fillZero: true,
379
+ };
380
+ case "zh-Hant":
381
+ return {
382
+ digits: Array.from("零壹貳參肆伍陸柒捌玖"),
383
+ small: ["拾", "佰", "仟"],
384
+ big: BIG_TRAD,
385
+ omitOne: false,
386
+ fillZero: true,
387
+ };
388
+ case "ko":
389
+ return {
390
+ digits: Array.from("零壹貳參四伍六七八九"),
391
+ small: ["拾", "百", "阡"],
392
+ big: BIG_TRAD,
393
+ omitOne: false,
394
+ fillZero: false,
395
+ };
396
+ }
397
+ }
398
+ if (level === 3) {
399
+ return {
400
+ digits: FULL_WIDTH,
401
+ small: ["十", "百", "千"],
402
+ big: level1Big,
403
+ omitOne: false,
404
+ fillZero: zh,
405
+ };
406
+ }
407
+ if (level === 4 && lang === "ko") {
408
+ return {
409
+ digits: Array.from("영일이삼사오육칠팔구"),
410
+ small: ["십", "백", "천"],
411
+ big: ["만", "억", "조"],
412
+ omitOne: true,
413
+ fillZero: false,
414
+ };
415
+ }
416
+ return {
417
+ digits: KANJI,
418
+ small: ["十", "百", "千"],
419
+ big: level1Big,
420
+ omitOne: !zh,
421
+ fillZero: zh,
422
+ };
423
+ };
424
+ const perDigit = (digits, dbnum) => {
425
+ const table = numerals(dbnum).digits;
426
+ return Array.from(digits)
427
+ .map((c) => (c >= "0" && c <= "9" ? table[Number(c)] : c))
428
+ .join("");
429
+ };
430
+ /** `digits` read as a number with units; past 兆 (≥ 10^16) digit by digit. */
431
+ const withUnits = (digits, dbnum) => {
432
+ const n = numerals(dbnum);
433
+ const trimmed = digits.replace(/^0+/, "");
434
+ if (!trimmed)
435
+ return n.digits[0];
436
+ if (trimmed.length > 16)
437
+ return perDigit(digits, dbnum);
438
+ const values = Array.from(trimmed, Number);
439
+ const groups = Math.ceil(values.length / 4);
440
+ let out = "";
441
+ let pendingZero = false;
442
+ for (let g = 0; g < groups; g += 1) {
443
+ const groupIndex = groups - 1 - g;
444
+ const end = values.length - groupIndex * 4;
445
+ const group = values.slice(Math.max(0, end - 4), end);
446
+ let wrote = false;
447
+ group.forEach((d, k) => {
448
+ const pos = group.length - 1 - k;
449
+ if (d === 0) {
450
+ pendingZero ||= out.length > 0 || wrote;
451
+ return;
452
+ }
453
+ if (pendingZero && n.fillZero)
454
+ out += "零";
455
+ pendingZero = false;
456
+ if (!(d === 1 && pos > 0 && n.omitOne))
457
+ out += n.digits[d];
458
+ if (pos > 0)
459
+ out += n.small[pos - 1];
460
+ wrote = true;
461
+ });
462
+ if (wrote && groupIndex > 0)
463
+ out += n.big[groupIndex - 1];
464
+ }
465
+ return out;
466
+ };
467
+ const isAsciiDigit = (c) => !!c && c >= "0" && c <= "9";
468
+ const convertDigits = (text, dbnum, mode) => {
469
+ const chars = Array.from(text);
470
+ let out = "";
471
+ let i = 0;
472
+ while (i < chars.length) {
473
+ if (!isAsciiDigit(chars[i])) {
474
+ out += chars[i];
475
+ i += 1;
476
+ continue;
477
+ }
478
+ let end = i;
479
+ while (end < chars.length && isAsciiDigit(chars[end]))
480
+ end += 1;
481
+ const run = chars.slice(i, end).join("");
482
+ const fraction = i >= 2 &&
483
+ (chars[i - 1] === "." || chars[i - 1] === ",") &&
484
+ isAsciiDigit(chars[i - 2]);
485
+ const units = mode === "date"
486
+ ? dbnum.level !== 3
487
+ : mode === "general"
488
+ ? !fraction
489
+ : false;
490
+ out += units ? withUnits(run, dbnum) : perDigit(run, dbnum);
491
+ i = end;
492
+ }
493
+ return out;
494
+ };
@@ -10,6 +10,15 @@ export declare const rgbaToArgbHex: ({ r, g, b, a }: RGBA) => string;
10
10
  export declare const isSameColor: (colorA: string | undefined, colorB: string | undefined) => boolean | undefined;
11
11
  export declare const colorStringToHex: (color: string | undefined) => string | undefined;
12
12
  export declare const lightenColor: (unit: number | undefined, color: string) => string | undefined;
13
+ /**
14
+ * Excel's tint (ECMA-376 Part 1, §18.8.19 `CT_Color@tint`) — what a `{ theme, tint }` colour paints
15
+ * as. Unlike `lightenColor`, which blends each channel toward black or white, the colour moves to
16
+ * HLS and only its luminance changes: `L·(1+tint)` for a negative tint, `L·(1−tint) + tint` for a
17
+ * positive one. Hue and saturation stay, so accent1 `#4472C4` at −25 % is Excel's `#2F5597` (not
18
+ * `#335693`) and accent2 `#ED7D31` at −25 % is `#C55A11`. The engine's `rnc_model::apply_tint`
19
+ * is the same rule. Returns `#rrggbb` (as `rgbToHex`); `undefined` for a colour it cannot read.
20
+ */
21
+ export declare const applyTint: (tint: number, color: string) => string | undefined;
13
22
  export declare const hexToRgb: (hex: string) => {
14
23
  r: number;
15
24
  g: number;
@@ -1 +1 @@
1
- {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../color.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,WAoCnD,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAYnE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAkBzD;AAED,eAAO,MAAM,aAAa,GAAI,gBAAoB,IAAI,WAUrD,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,MAAM,GAAG,SAAS,wBAiB3B,CAAC;AAQF,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,GAAG,SAAS,uBAczD,CAAC;AAGF,eAAO,MAAM,YAAY,GAAI,wBAAU,EAAE,OAAO,MAAM,uBAGrD,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM;;;;CAOnC,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,WAGvD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAAC;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAUA,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,QAAQ,MAAM,EACd,QAAQ,MAAM,KACb,MAUF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,SAAS,MAAM,EAAE,EAC1B,OAAO,MAAM,KACZ,MAMF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,MAAM,EAAE,KAAG,MAAM,EAInE,CAAC;AAEJ,eAAO,MAAM,gBAAgB,GAC3B,iBAAiB,MAAM,EACvB,YAAY,MAAM,EAClB,WAAW,MAAM,uBAUlB,CAAC"}
1
+ {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../color.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,WAoCnD,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAYnE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAkBzD;AAED,eAAO,MAAM,aAAa,GAAI,gBAAoB,IAAI,WAUrD,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,MAAM,GAAG,SAAS,wBAiB3B,CAAC;AAQF,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,GAAG,SAAS,uBAczD,CAAC;AAGF,eAAO,MAAM,YAAY,GAAI,wBAAU,EAAE,OAAO,MAAM,uBAGrD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,MAAM,GAAG,SAYhE,CAAC;AA8BF,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM;;;;CAOnC,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,WAGvD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAAC;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAUA,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,QAAQ,MAAM,EACd,QAAQ,MAAM,KACb,MAUF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,SAAS,MAAM,EAAE,EAC1B,OAAO,MAAM,KACZ,MAMF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,MAAM,EAAE,KAAG,MAAM,EAInE,CAAC;AAEJ,eAAO,MAAM,gBAAgB,GAC3B,iBAAiB,MAAM,EACvB,YAAY,MAAM,EAClB,WAAW,MAAM,uBAUlB,CAAC"}
package/dist/esm/color.js CHANGED
@@ -114,6 +114,59 @@ export const lightenColor = (unit = 0.2, color) => {
114
114
  const rgbColor = normalizeColor(color);
115
115
  return rgbColor ? shade(unit, rgbColor) : undefined;
116
116
  };
117
+ /**
118
+ * Excel's tint (ECMA-376 Part 1, §18.8.19 `CT_Color@tint`) — what a `{ theme, tint }` colour paints
119
+ * as. Unlike `lightenColor`, which blends each channel toward black or white, the colour moves to
120
+ * HLS and only its luminance changes: `L·(1+tint)` for a negative tint, `L·(1−tint) + tint` for a
121
+ * positive one. Hue and saturation stay, so accent1 `#4472C4` at −25 % is Excel's `#2F5597` (not
122
+ * `#335693`) and accent2 `#ED7D31` at −25 % is `#C55A11`. The engine's `rnc_model::apply_tint`
123
+ * is the same rule. Returns `#rrggbb` (as `rgbToHex`); `undefined` for a colour it cannot read.
124
+ */
125
+ export const applyTint = (tint, color) => {
126
+ const rgba = normalizeColor(color);
127
+ if (!rgba || rgba.a === 0)
128
+ return undefined;
129
+ if (!Number.isFinite(tint) || tint === 0) {
130
+ return rgbToHex(Math.round(rgba.r), Math.round(rgba.g), Math.round(rgba.b));
131
+ }
132
+ const t = Math.max(-1, Math.min(1, tint));
133
+ const [h, l, s] = rgbToHls(rgba.r / 255, rgba.g / 255, rgba.b / 255);
134
+ const lum = Math.max(0, Math.min(1, t < 0 ? l * (1 + t) : l * (1 - t) + t));
135
+ const [r, g, b] = hlsToRgb(h, lum, s);
136
+ const byte = (c) => Math.max(0, Math.min(255, Math.round(c * 255)));
137
+ return rgbToHex(byte(r), byte(g), byte(b));
138
+ };
139
+ const rgbToHls = (r, g, b) => {
140
+ const max = Math.max(r, g, b);
141
+ const min = Math.min(r, g, b);
142
+ const l = (max + min) / 2;
143
+ if (max === min)
144
+ return [0, l, 0];
145
+ const range = max - min;
146
+ const s = l <= 0.5 ? range / (max + min) : range / (2 - max - min);
147
+ const rc = (max - r) / range;
148
+ const gc = (max - g) / range;
149
+ const bc = (max - b) / range;
150
+ const h = r === max ? bc - gc : g === max ? 2 + rc - bc : 4 + gc - rc;
151
+ return [(((h / 6) % 1) + 1) % 1, l, s];
152
+ };
153
+ const hlsToRgb = (h, l, s) => {
154
+ if (s === 0)
155
+ return [l, l, l];
156
+ const m2 = l <= 0.5 ? l * (1 + s) : l + s - l * s;
157
+ const m1 = 2 * l - m2;
158
+ const v = (hue) => {
159
+ const x = ((hue % 1) + 1) % 1;
160
+ if (x < 1 / 6)
161
+ return m1 + (m2 - m1) * x * 6;
162
+ if (x < 0.5)
163
+ return m2;
164
+ if (x < 2 / 3)
165
+ return m1 + (m2 - m1) * (2 / 3 - x) * 6;
166
+ return m1;
167
+ };
168
+ return [v(h + 1 / 3), v(h), v(h - 1 / 3)];
169
+ };
117
170
  export const hexToRgb = (hex) => {
118
171
  const bigint = parseInt(hex.slice(1), 16);
119
172
  const r = (bigint >> 16) & 255;
@@ -1,10 +1,38 @@
1
- import type { DataValidationRule } from "@rowsncolumns/common-types";
1
+ import type { BooleanCondition, ConditionType, DataValidationRule } from "@rowsncolumns/common-types";
2
2
  /**
3
3
  * A leading apostrophe in a data-validation / conditional-format list entry is
4
4
  * Excel's text prefix — `'000s` is the text "000s" and a bare `'` is empty
5
5
  * text — never part of the value itself.
6
6
  */
7
7
  export declare const stripDataValidationTextPrefix: (entry: string) => string;
8
+ /**
9
+ * A `ONE_OF_LIST` value that is a SOURCE FORMULA rather than literal items,
10
+ * returned trimmed with its leading `=` (`=RegionList`, `=$A$2:$A$9`,
11
+ * `=Sheet2!A1:A5`, `=INDIRECT("…")`). Excel's List type has one Source box
12
+ * that takes either literal items (`Yes,No`) or a formula starting with `=`;
13
+ * the "Dropdown" (`ONE_OF_LIST`) / "Dropdown (from a range)" (`ONE_OF_RANGE`)
14
+ * split means a `=…` typed into the literal dropdown is read the way Excel
15
+ * reads it — as a `ONE_OF_RANGE` over that source. A quoted literal
16
+ * (`"=a,b"`, Excel's quoted list), a `=` inside a later item (`a,=b`) and a
17
+ * bare `=` stay literal. Rust twin: `rnc_model::validation::one_of_list_formula_source`.
18
+ */
19
+ export declare const getDataValidationListFormulaSource: (value: string | undefined | null) => string | undefined;
20
+ /**
21
+ * The condition type a rule behaves as: a `ONE_OF_LIST` whose first value is
22
+ * a source formula (`getDataValidationListFormulaSource`) behaves as
23
+ * `ONE_OF_RANGE`; every other condition is its own type. Rust twin:
24
+ * `rnc_model::validation::effective_condition_kind`.
25
+ */
26
+ export declare const getEffectiveDataValidationConditionType: (condition: BooleanCondition | undefined | null) => ConditionType | undefined;
27
+ /**
28
+ * The reference a list rule draws its options from, or `undefined` when the
29
+ * rule is a literal item list (or not a list rule at all). `ONE_OF_RANGE` is
30
+ * always a reference (its value, `""` when missing); a `ONE_OF_LIST` is one
31
+ * when its value is a source formula (`=RegionList`) or a bare A1 reference
32
+ * (`$A$1:$A$5`). Shared by the dropdown options, the commit gate and
33
+ * `checkBooleanRule`, so the three always agree.
34
+ */
35
+ export declare const getDataValidationListReference: (condition: BooleanCondition | undefined | null) => string | undefined;
8
36
  /**
9
37
  * Whether a cell's current text matches a ONE_OF_LIST option, with Excel's
10
38
  * text-prefix semantics on BOTH sides: the option strips its leading `'`
@@ -1 +1 @@
1
- {"version":3,"file":"data-validation.d.ts","sourceRoot":"","sources":["../../data-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AASpC;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,OAAO,MAAM,KAAG,MACd,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAC/C,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,EACjC,WAAW,MAAM,GAAG,SAAS,GAAG,IAAI,KACnC,OAKF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,kBAAkB,GAAG,SAAS,YAKrC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACxC,MAAM,kBAAkB,GAAG,SAAS,EACpC,aAAa,MAAM,EACnB,SAAS,MAAM,uBAgBhB,CAAC"}
1
+ {"version":3,"file":"data-validation.d.ts","sourceRoot":"","sources":["../../data-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAUpC;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,OAAO,MAAM,KAAG,MACd,CAAC;AAEjD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,GAC7C,OAAO,MAAM,GAAG,SAAS,GAAG,IAAI,KAC/B,MAAM,GAAG,SAKX,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uCAAuC,GAClD,WAAW,gBAAgB,GAAG,SAAS,GAAG,IAAI,KAC7C,aAAa,GAAG,SAIe,CAAC;AAEnC;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,GACzC,WAAW,gBAAgB,GAAG,SAAS,GAAG,IAAI,KAC7C,MAAM,GAAG,SAYX,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAC/C,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,EACjC,WAAW,MAAM,GAAG,SAAS,GAAG,IAAI,KACnC,OAKF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,kBAAkB,GAAG,SAAS,YAKrC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACxC,MAAM,kBAAkB,GAAG,SAAS,EACpC,aAAa,MAAM,EACnB,SAAS,MAAM,uBAgBhB,CAAC"}