@forthtilliath/ts-kit 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -8
- package/dist/array/index.d.ts +2 -0
- package/dist/array/index.d.ts.map +1 -1
- package/dist/array/index.js +2 -0
- package/dist/array/indexById.d.ts +15 -0
- package/dist/array/indexById.d.ts.map +1 -0
- package/dist/array/indexById.js +14 -0
- package/dist/array/moveItem.d.ts +18 -0
- package/dist/array/moveItem.d.ts.map +1 -0
- package/dist/array/moveItem.js +9 -0
- package/dist/color/contrastRatio.d.ts +40 -0
- package/dist/color/contrastRatio.d.ts.map +1 -0
- package/dist/color/contrastRatio.js +62 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/color/index.d.ts.map +1 -0
- package/dist/color/index.js +1 -0
- package/dist/csv/escapeCsvField.d.ts +6 -2
- package/dist/csv/escapeCsvField.d.ts.map +1 -1
- package/dist/csv/escapeCsvField.js +13 -8
- package/dist/csv/index.d.ts +1 -0
- package/dist/csv/index.d.ts.map +1 -1
- package/dist/csv/index.js +1 -0
- package/dist/csv/neutralizeCsvFormula.d.ts +27 -0
- package/dist/csv/neutralizeCsvFormula.d.ts.map +1 -0
- package/dist/csv/neutralizeCsvFormula.js +25 -0
- package/dist/csv/toCsv.d.ts +6 -1
- package/dist/csv/toCsv.d.ts.map +1 -1
- package/dist/csv/toCsv.js +10 -4
- package/dist/date/formatRelativeTime.d.ts +2 -0
- package/dist/date/formatRelativeTime.d.ts.map +1 -1
- package/dist/date/formatRelativeTime.js +37 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/markdown/parseChangelogNotes.d.ts.map +1 -1
- package/dist/markdown/parseChangelogNotes.js +9 -2
- package/dist/string/pluralize.d.ts +3 -1
- package/dist/string/pluralize.d.ts.map +1 -1
- package/dist/string/pluralize.js +17 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -57,6 +57,11 @@ Importing them is harmless anywhere: the DOM is only touched when called.
|
|
|
57
57
|
- `getMostRecentIds(rows, limit?)` — most recently occurring distinct ids
|
|
58
58
|
(`{ id, occurredAt }[]`), most recent first, deduplicated, limited
|
|
59
59
|
(default 5).
|
|
60
|
+
- `indexById(items)` — `ReadonlyMap` from each item's `id` to the item (last
|
|
61
|
+
wins on duplicates), to build once instead of a `find` per rendered row.
|
|
62
|
+
- `moveItem(items, from, offset)` — copy of `items` with the element at
|
|
63
|
+
`from` moved `offset` places (negative = towards the start); returns
|
|
64
|
+
`items` itself when nothing moves, so a state setter can skip the update.
|
|
60
65
|
- `nextInCycle(ids, currentId)` — next id in a short list (e.g. cycling
|
|
61
66
|
through recents on tap) — wraps to the first if `currentId` is at the end
|
|
62
67
|
or no longer in the list.
|
|
@@ -114,16 +119,35 @@ Importing them is harmless anywhere: the DOM is only touched when called.
|
|
|
114
119
|
rarely needed since `FArray` is already assignable to `T[]` anywhere a
|
|
115
120
|
plain array is expected, but useful when the runtime type matters too.
|
|
116
121
|
|
|
122
|
+
### `color`
|
|
123
|
+
|
|
124
|
+
- `contrastRatio(a, b)` — WCAG 2 contrast ratio between two hex colours
|
|
125
|
+
(`#rgb`/`#rrggbb`), from 1 to 21; compare with `TEXT_CONTRAST` (4.5) or
|
|
126
|
+
`GRAPHIC_CONTRAST` (3). Throws a `TypeError` on an invalid colour.
|
|
127
|
+
- `relativeLuminance(hex)` — WCAG 2 relative luminance, 0 (black) to 1.
|
|
128
|
+
- `readableTextOn(background)` — `"#ffffff"` or `"#000000"`, whichever reads
|
|
129
|
+
best on that background.
|
|
130
|
+
|
|
117
131
|
### `csv`
|
|
118
132
|
|
|
133
|
+
Every text cell that starts with `=`, `+`, `-`, `@`, a tab or a carriage
|
|
134
|
+
return is prefixed with `'` by `toCsv` and `escapeCsvField`, so Excel,
|
|
135
|
+
LibreOffice or Sheets show it instead of running it as a formula (CSV
|
|
136
|
+
injection: `=HYPERLINK(…)` typed as a name becomes a trapped link). Numbers,
|
|
137
|
+
and numbers written as text (`"-3"`), are left as they are. Pass
|
|
138
|
+
`{ neutralizeFormulas: false }` only for a file no spreadsheet opens.
|
|
139
|
+
|
|
119
140
|
- `parseCsvLine(line, delimiter?)` — splits one CSV line into trimmed cells;
|
|
120
141
|
quoted cells may contain the delimiter, `""` stands for a literal quote.
|
|
121
142
|
Delimiter defaults to `","`.
|
|
122
|
-
- `toCsv(rows, delimiter?)` — serializes whole rows
|
|
123
|
-
and escaped, rows joined with `\n`. Delimiter
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
- `toCsv(rows, delimiter?, { neutralizeFormulas? })` — serializes whole rows
|
|
144
|
+
to CSV, every cell quoted and escaped, rows joined with `\n`. Delimiter
|
|
145
|
+
defaults to `","`.
|
|
146
|
+
- `escapeCsvField(value, delimiter?, { neutralizeFormulas? })` — escapes a
|
|
147
|
+
single field (RFC 4180), quoting it only if it contains a `"`, the
|
|
148
|
+
delimiter or a line break — to build rows cell by cell. Delimiter defaults
|
|
149
|
+
to `";"` (French-locale Excel).
|
|
150
|
+
- `neutralizeCsvFormula(value)` — the formula guard above, on its own.
|
|
127
151
|
- `formatCsvNumber(value, decimals?)` — formats a number with a comma decimal
|
|
128
152
|
separator (French-locale spreadsheets) instead of JS's dot.
|
|
129
153
|
- `downloadCsv(filename, content)` — browser download as `text/csv`, with a
|
|
@@ -144,7 +168,9 @@ Importing them is harmless anywhere: the DOM is only touched when called.
|
|
|
144
168
|
- `mondayFirstWeekday(date)` — weekday index with Monday = `0` … Sunday = `6`,
|
|
145
169
|
from a `Date` or a `YYYY-MM-DD` string; offsets a Monday-first month grid.
|
|
146
170
|
- `formatRelativeTime(date, baseDate?, locale?)` — human-readable relative
|
|
147
|
-
time ("3 hours ago"), backed by `Intl.RelativeTimeFormat`.
|
|
171
|
+
time ("3 hours ago"), backed by `Intl.RelativeTimeFormat`. Without it
|
|
172
|
+
(Hermes, React Native on Android), a built-in French/English fallback
|
|
173
|
+
("il y a 3 heures"; other locales in English) instead of a crash.
|
|
148
174
|
- `formatRecentDate(date, { now?, locale? })` — a past moment as a "last
|
|
149
175
|
saved…" line reads best: `"aujourd'hui à 21:42"`, `"hier à 08:05"`, then
|
|
150
176
|
`"le 11 octobre 2026"` (`locale: "en"`: `"today at…"`, `"on October 11, 2026"`).
|
|
@@ -210,7 +236,7 @@ Building blocks for writing an iCalendar (`.ics`, RFC 5545) file.
|
|
|
210
236
|
### `markdown`
|
|
211
237
|
|
|
212
238
|
- `parseChangelogNotes(notes)` — parses a small subset of Markdown
|
|
213
|
-
(
|
|
239
|
+
(`#` to `###### heading`, `- item`, `**bold**`) commonly found in GitHub release
|
|
214
240
|
notes into a list of typed blocks (`heading` / `item` / `text`, each with
|
|
215
241
|
`bold`-aware segments), ready for a UI layer to render without a full
|
|
216
242
|
Markdown dependency.
|
|
@@ -250,7 +276,9 @@ Building blocks for writing an iCalendar (`.ics`, RFC 5545) file.
|
|
|
250
276
|
- `pluralize(count, singular, plural? | { plural?, locale? })` — picks the
|
|
251
277
|
singular or the plural form. Which counts take the singular follows the
|
|
252
278
|
locale's rules (`Intl.PluralRules`, `"en"` by default): only ±1 in
|
|
253
|
-
English, 0 and 1.5 too in French.
|
|
279
|
+
English, 0 and 1.5 too in French. Where `Intl.PluralRules` is missing
|
|
280
|
+
(Hermes, React Native on Android), French keeps the singular below 2 and
|
|
281
|
+
other languages only for ±1. Without an explicit plural form, a naive
|
|
254
282
|
suffix: English `s`/`es`/`ies`, French `s` (nothing after `s`/`x`/`z`),
|
|
255
283
|
`s` elsewhere.
|
|
256
284
|
- `createPluralize(locale)` — `pluralize` bound to a locale, declared once per
|
package/dist/array/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * from "./chunk.js";
|
|
|
2
2
|
export * from "./createColumnStorage.js";
|
|
3
3
|
export * from "./flattenDeep.js";
|
|
4
4
|
export * from "./getMostRecentIds.js";
|
|
5
|
+
export * from "./indexById.js";
|
|
6
|
+
export * from "./moveItem.js";
|
|
5
7
|
export * from "./nextInCycle.js";
|
|
6
8
|
export * from "./rankByNameMatch.js";
|
|
7
9
|
export * from "./shuffle.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
|
package/dist/array/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export * from "./chunk.js";
|
|
|
2
2
|
export * from "./createColumnStorage.js";
|
|
3
3
|
export * from "./flattenDeep.js";
|
|
4
4
|
export * from "./getMostRecentIds.js";
|
|
5
|
+
export * from "./indexById.js";
|
|
6
|
+
export * from "./moveItem.js";
|
|
5
7
|
export * from "./nextInCycle.js";
|
|
6
8
|
export * from "./rankByNameMatch.js";
|
|
7
9
|
export * from "./shuffle.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indexes items by their `id`, to build once (e.g. in a `useMemo`) and read
|
|
3
|
+
* in O(1) instead of a `find` per rendered element (games × their players).
|
|
4
|
+
* With duplicate ids, the last item wins.
|
|
5
|
+
*
|
|
6
|
+
* @param items - The items to index.
|
|
7
|
+
* @returns A read-only map from each `id` to its item.
|
|
8
|
+
* @example
|
|
9
|
+
* const players = indexById([{ id: "a", name: "Ann" }]);
|
|
10
|
+
* players.get("a")?.name; // => "Ann"
|
|
11
|
+
*/
|
|
12
|
+
export declare function indexById<T extends {
|
|
13
|
+
id: unknown;
|
|
14
|
+
}>(items: readonly T[]): ReadonlyMap<T["id"], T>;
|
|
15
|
+
//# sourceMappingURL=indexById.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexById.d.ts","sourceRoot":"","sources":["../../src/array/indexById.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,EACjD,KAAK,EAAE,SAAS,CAAC,EAAE,GAClB,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAEzB"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indexes items by their `id`, to build once (e.g. in a `useMemo`) and read
|
|
3
|
+
* in O(1) instead of a `find` per rendered element (games × their players).
|
|
4
|
+
* With duplicate ids, the last item wins.
|
|
5
|
+
*
|
|
6
|
+
* @param items - The items to index.
|
|
7
|
+
* @returns A read-only map from each `id` to its item.
|
|
8
|
+
* @example
|
|
9
|
+
* const players = indexById([{ id: "a", name: "Ann" }]);
|
|
10
|
+
* players.get("a")?.name; // => "Ann"
|
|
11
|
+
*/
|
|
12
|
+
export function indexById(items) {
|
|
13
|
+
return new Map(items.map((item) => [item.id, item]));
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a copy of `items` with the element at `from` moved `offset` places
|
|
3
|
+
* (negative = towards the start), the others shifting to make room. When
|
|
4
|
+
* nothing moves (`from` or the destination out of bounds, `offset` 0), the
|
|
5
|
+
* very same array is returned, so a state setter can skip a useless update.
|
|
6
|
+
*
|
|
7
|
+
* @param items - The array to copy.
|
|
8
|
+
* @param from - The position of the element to move.
|
|
9
|
+
* @param offset - How many places to move it.
|
|
10
|
+
* @returns A new array with the element moved, or `items` itself.
|
|
11
|
+
* @example
|
|
12
|
+
* moveItem(["a", "b", "c", "d"], 0, 2); // => ["b", "c", "a", "d"]
|
|
13
|
+
* moveItem(["a", "b", "c"], 2, -1); // => ["a", "c", "b"]
|
|
14
|
+
* moveItem(list, 0, -1) === list; // => true
|
|
15
|
+
*/
|
|
16
|
+
export declare function moveItem<T>(items: T[], from: number, offset: number): T[];
|
|
17
|
+
export declare function moveItem<T>(items: readonly T[], from: number, offset: number): readonly T[];
|
|
18
|
+
//# sourceMappingURL=moveItem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moveItem.d.ts","sourceRoot":"","sources":["../../src/array/moveItem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;AAC3E,wBAAgB,QAAQ,CAAC,CAAC,EACxB,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,SAAS,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function moveItem(items, from, offset) {
|
|
2
|
+
const to = from + offset;
|
|
3
|
+
const inBounds = (i) => Number.isInteger(i) && i >= 0 && i < items.length;
|
|
4
|
+
if (!inBounds(from) || !inBounds(to) || to === from)
|
|
5
|
+
return items;
|
|
6
|
+
const next = [...items];
|
|
7
|
+
next.splice(to, 0, ...next.splice(from, 1));
|
|
8
|
+
return next;
|
|
9
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** WCAG 2 AA minimum for body text. */
|
|
2
|
+
export declare const TEXT_CONTRAST = 4.5;
|
|
3
|
+
/** WCAG 2 AA minimum for large text and graphical objects (icons, bars). */
|
|
4
|
+
export declare const GRAPHIC_CONTRAST = 3;
|
|
5
|
+
/**
|
|
6
|
+
* The WCAG 2 relative luminance of a colour, from 0 (black) to 1 (white).
|
|
7
|
+
*
|
|
8
|
+
* @param hex - A `#rgb` or `#rrggbb` colour (`#` optional).
|
|
9
|
+
* @returns Its relative luminance.
|
|
10
|
+
* @throws {TypeError} If `hex` is not a 3- or 6-digit hex colour.
|
|
11
|
+
* @example
|
|
12
|
+
* relativeLuminance("#ffffff"); // => 1
|
|
13
|
+
*/
|
|
14
|
+
export declare function relativeLuminance(hex: string): number;
|
|
15
|
+
/**
|
|
16
|
+
* The WCAG 2 contrast ratio between two colours, from 1 (identical) to 21
|
|
17
|
+
* (black on white), whatever their order. Compare it with `TEXT_CONTRAST`
|
|
18
|
+
* (4.5) or `GRAPHIC_CONTRAST` (3).
|
|
19
|
+
*
|
|
20
|
+
* @param a - A `#rgb` or `#rrggbb` colour.
|
|
21
|
+
* @param b - Another one.
|
|
22
|
+
* @returns The contrast ratio.
|
|
23
|
+
* @throws {TypeError} If either colour is not a 3- or 6-digit hex colour.
|
|
24
|
+
* @example
|
|
25
|
+
* contrastRatio("#000", "#fff"); // => 21
|
|
26
|
+
* contrastRatio("#767676", "#ffffff") >= TEXT_CONTRAST; // => true
|
|
27
|
+
*/
|
|
28
|
+
export declare function contrastRatio(a: string, b: string): number;
|
|
29
|
+
/**
|
|
30
|
+
* Black or white, whichever reads best on `background` (pure black or white
|
|
31
|
+
* always reaches at least 4.58:1 on some side).
|
|
32
|
+
*
|
|
33
|
+
* @param background - A `#rgb` or `#rrggbb` colour.
|
|
34
|
+
* @returns `"#ffffff"` or `"#000000"`.
|
|
35
|
+
* @example
|
|
36
|
+
* readableTextOn("#16283d"); // => "#ffffff"
|
|
37
|
+
* readableTextOn("#84cc16"); // => "#000000"
|
|
38
|
+
*/
|
|
39
|
+
export declare function readableTextOn(background: string): "#ffffff" | "#000000";
|
|
40
|
+
//# sourceMappingURL=contrastRatio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contrastRatio.d.ts","sourceRoot":"","sources":["../../src/color/contrastRatio.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAgBlC;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMrD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAKxE"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** WCAG 2 AA minimum for body text. */
|
|
2
|
+
export const TEXT_CONTRAST = 4.5;
|
|
3
|
+
/** WCAG 2 AA minimum for large text and graphical objects (icons, bars). */
|
|
4
|
+
export const GRAPHIC_CONTRAST = 3;
|
|
5
|
+
const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
6
|
+
function channels(hex) {
|
|
7
|
+
const match = HEX.exec(hex.trim());
|
|
8
|
+
if (!match?.[1])
|
|
9
|
+
throw new TypeError(`Invalid hex colour: "${hex}"`);
|
|
10
|
+
const digits = match[1].length === 3 ? match[1].replace(/./g, "$&$&") : match[1];
|
|
11
|
+
return [0, 2, 4].map((i) => Number.parseInt(digits.slice(i, i + 2), 16));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The WCAG 2 relative luminance of a colour, from 0 (black) to 1 (white).
|
|
15
|
+
*
|
|
16
|
+
* @param hex - A `#rgb` or `#rrggbb` colour (`#` optional).
|
|
17
|
+
* @returns Its relative luminance.
|
|
18
|
+
* @throws {TypeError} If `hex` is not a 3- or 6-digit hex colour.
|
|
19
|
+
* @example
|
|
20
|
+
* relativeLuminance("#ffffff"); // => 1
|
|
21
|
+
*/
|
|
22
|
+
export function relativeLuminance(hex) {
|
|
23
|
+
const [r, g, b] = channels(hex).map((channel) => {
|
|
24
|
+
const c = channel / 255;
|
|
25
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
26
|
+
});
|
|
27
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The WCAG 2 contrast ratio between two colours, from 1 (identical) to 21
|
|
31
|
+
* (black on white), whatever their order. Compare it with `TEXT_CONTRAST`
|
|
32
|
+
* (4.5) or `GRAPHIC_CONTRAST` (3).
|
|
33
|
+
*
|
|
34
|
+
* @param a - A `#rgb` or `#rrggbb` colour.
|
|
35
|
+
* @param b - Another one.
|
|
36
|
+
* @returns The contrast ratio.
|
|
37
|
+
* @throws {TypeError} If either colour is not a 3- or 6-digit hex colour.
|
|
38
|
+
* @example
|
|
39
|
+
* contrastRatio("#000", "#fff"); // => 21
|
|
40
|
+
* contrastRatio("#767676", "#ffffff") >= TEXT_CONTRAST; // => true
|
|
41
|
+
*/
|
|
42
|
+
export function contrastRatio(a, b) {
|
|
43
|
+
const la = relativeLuminance(a);
|
|
44
|
+
const lb = relativeLuminance(b);
|
|
45
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Black or white, whichever reads best on `background` (pure black or white
|
|
49
|
+
* always reaches at least 4.58:1 on some side).
|
|
50
|
+
*
|
|
51
|
+
* @param background - A `#rgb` or `#rrggbb` colour.
|
|
52
|
+
* @returns `"#ffffff"` or `"#000000"`.
|
|
53
|
+
* @example
|
|
54
|
+
* readableTextOn("#16283d"); // => "#ffffff"
|
|
55
|
+
* readableTextOn("#84cc16"); // => "#000000"
|
|
56
|
+
*/
|
|
57
|
+
export function readableTextOn(background) {
|
|
58
|
+
return contrastRatio("#ffffff", background) >=
|
|
59
|
+
contrastRatio("#000000", background)
|
|
60
|
+
? "#ffffff"
|
|
61
|
+
: "#000000";
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/color/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./contrastRatio.js";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type CsvFormulaOptions } from "./neutralizeCsvFormula.js";
|
|
1
2
|
/**
|
|
2
3
|
* Escapes a single CSV field per RFC 4180, quoting it only when needed: if it
|
|
3
4
|
* contains a double quote, the delimiter or a line break, it's wrapped in
|
|
4
5
|
* double quotes with any internal double quote doubled; otherwise it's
|
|
5
|
-
* returned as-is.
|
|
6
|
+
* returned as-is. A field that would run as a spreadsheet formula (`=`, `+`,
|
|
7
|
+
* `-`, `@`…) is first prefixed with `'` (see `neutralizeCsvFormula`).
|
|
6
8
|
*
|
|
7
9
|
* Builds rows cell by cell. To serialize whole rows at once, see `toCsv`
|
|
8
10
|
* (which always quotes, and defaults to `,`).
|
|
@@ -10,12 +12,14 @@
|
|
|
10
12
|
* @param value - The raw field value.
|
|
11
13
|
* @param delimiter - The cell separator the field will be joined with.
|
|
12
14
|
* Defaults to `";"`, what French-locale Excel expects.
|
|
15
|
+
* @param options - `{ neutralizeFormulas }`, `true` by default.
|
|
13
16
|
* @returns The value, quoted and escaped only if needed.
|
|
14
17
|
* @example
|
|
15
18
|
* escapeCsvField("Apple"); // => "Apple"
|
|
16
19
|
* escapeCsvField("a;b"); // => '"a;b"'
|
|
17
20
|
* escapeCsvField("a,b", ","); // => '"a,b"'
|
|
18
21
|
* escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
|
|
22
|
+
* escapeCsvField("=1+1"); // => "'=1+1"
|
|
19
23
|
*/
|
|
20
|
-
export declare function escapeCsvField(value: string, delimiter?: string): string;
|
|
24
|
+
export declare function escapeCsvField(value: string, delimiter?: string, { neutralizeFormulas }?: CsvFormulaOptions): string;
|
|
21
25
|
//# sourceMappingURL=escapeCsvField.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/csv/escapeCsvField.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/csv/escapeCsvField.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EAEvB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,SAAS,SAAM,EACf,EAAE,kBAAyB,EAAE,GAAE,iBAAsB,GACpD,MAAM,CASR"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { neutralizeCsvFormula, } from "./neutralizeCsvFormula.js";
|
|
1
2
|
/**
|
|
2
3
|
* Escapes a single CSV field per RFC 4180, quoting it only when needed: if it
|
|
3
4
|
* contains a double quote, the delimiter or a line break, it's wrapped in
|
|
4
5
|
* double quotes with any internal double quote doubled; otherwise it's
|
|
5
|
-
* returned as-is.
|
|
6
|
+
* returned as-is. A field that would run as a spreadsheet formula (`=`, `+`,
|
|
7
|
+
* `-`, `@`…) is first prefixed with `'` (see `neutralizeCsvFormula`).
|
|
6
8
|
*
|
|
7
9
|
* Builds rows cell by cell. To serialize whole rows at once, see `toCsv`
|
|
8
10
|
* (which always quotes, and defaults to `,`).
|
|
@@ -10,19 +12,22 @@
|
|
|
10
12
|
* @param value - The raw field value.
|
|
11
13
|
* @param delimiter - The cell separator the field will be joined with.
|
|
12
14
|
* Defaults to `";"`, what French-locale Excel expects.
|
|
15
|
+
* @param options - `{ neutralizeFormulas }`, `true` by default.
|
|
13
16
|
* @returns The value, quoted and escaped only if needed.
|
|
14
17
|
* @example
|
|
15
18
|
* escapeCsvField("Apple"); // => "Apple"
|
|
16
19
|
* escapeCsvField("a;b"); // => '"a;b"'
|
|
17
20
|
* escapeCsvField("a,b", ","); // => '"a,b"'
|
|
18
21
|
* escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
|
|
22
|
+
* escapeCsvField("=1+1"); // => "'=1+1"
|
|
19
23
|
*/
|
|
20
|
-
export function escapeCsvField(value, delimiter = ";") {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
export function escapeCsvField(value, delimiter = ";", { neutralizeFormulas = true } = {}) {
|
|
25
|
+
const safe = neutralizeFormulas ? neutralizeCsvFormula(value) : value;
|
|
26
|
+
const needsQuoting = safe.includes('"') ||
|
|
27
|
+
safe.includes(delimiter) ||
|
|
28
|
+
safe.includes("\n") ||
|
|
29
|
+
safe.includes("\r");
|
|
25
30
|
if (!needsQuoting)
|
|
26
|
-
return
|
|
27
|
-
return `"${
|
|
31
|
+
return safe;
|
|
32
|
+
return `"${safe.replace(/"/g, '""')}"`;
|
|
28
33
|
}
|
package/dist/csv/index.d.ts
CHANGED
package/dist/csv/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csv/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csv/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
|
package/dist/csv/index.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defuses CSV/formula injection: a text cell that starts with `=`, `+`, `-`,
|
|
3
|
+
* `@`, a tab or a carriage return is run as a formula by Excel, LibreOffice
|
|
4
|
+
* or Sheets (`=HYPERLINK("http://…","click")` becomes a trapped link). Such a
|
|
5
|
+
* cell is prefixed with an apostrophe so the spreadsheet shows it as text.
|
|
6
|
+
* Numbers written as text (`"-3"`, `"+1,5"`) are left untouched.
|
|
7
|
+
*
|
|
8
|
+
* `toCsv` and `escapeCsvField` already call it by default.
|
|
9
|
+
*
|
|
10
|
+
* @param value - The raw cell value.
|
|
11
|
+
* @returns The value, prefixed with `'` if it would run as a formula.
|
|
12
|
+
* @example
|
|
13
|
+
* neutralizeCsvFormula("=1+1"); // => "'=1+1"
|
|
14
|
+
* neutralizeCsvFormula("@SUM(A1)"); // => "'@SUM(A1)"
|
|
15
|
+
* neutralizeCsvFormula("-3"); // => "-3"
|
|
16
|
+
* neutralizeCsvFormula("Ann"); // => "Ann"
|
|
17
|
+
*/
|
|
18
|
+
export declare function neutralizeCsvFormula(value: string): string;
|
|
19
|
+
export interface CsvFormulaOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Prefixes with `'` the text cells a spreadsheet would run as formulas (see
|
|
22
|
+
* `neutralizeCsvFormula`). Defaults to `true`; turn it off only for a file
|
|
23
|
+
* no spreadsheet will open, or whose cells are not user input.
|
|
24
|
+
*/
|
|
25
|
+
neutralizeFormulas?: boolean;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=neutralizeCsvFormula.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"neutralizeCsvFormula.d.ts","sourceRoot":"","sources":["../../src/csv/neutralizeCsvFormula.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A number written as text ("-3", "+33 6", "1,5", "2e-3") stays a number.
|
|
2
|
+
const NUMERIC = /^[+-]?(\d+([.,]\d*)?|[.,]\d+)([eE][+-]?\d+)?$/;
|
|
3
|
+
const FORMULA_START = /^[=+\-@\t\r]/;
|
|
4
|
+
/**
|
|
5
|
+
* Defuses CSV/formula injection: a text cell that starts with `=`, `+`, `-`,
|
|
6
|
+
* `@`, a tab or a carriage return is run as a formula by Excel, LibreOffice
|
|
7
|
+
* or Sheets (`=HYPERLINK("http://…","click")` becomes a trapped link). Such a
|
|
8
|
+
* cell is prefixed with an apostrophe so the spreadsheet shows it as text.
|
|
9
|
+
* Numbers written as text (`"-3"`, `"+1,5"`) are left untouched.
|
|
10
|
+
*
|
|
11
|
+
* `toCsv` and `escapeCsvField` already call it by default.
|
|
12
|
+
*
|
|
13
|
+
* @param value - The raw cell value.
|
|
14
|
+
* @returns The value, prefixed with `'` if it would run as a formula.
|
|
15
|
+
* @example
|
|
16
|
+
* neutralizeCsvFormula("=1+1"); // => "'=1+1"
|
|
17
|
+
* neutralizeCsvFormula("@SUM(A1)"); // => "'@SUM(A1)"
|
|
18
|
+
* neutralizeCsvFormula("-3"); // => "-3"
|
|
19
|
+
* neutralizeCsvFormula("Ann"); // => "Ann"
|
|
20
|
+
*/
|
|
21
|
+
export function neutralizeCsvFormula(value) {
|
|
22
|
+
if (!FORMULA_START.test(value) || NUMERIC.test(value))
|
|
23
|
+
return value;
|
|
24
|
+
return `'${value}`;
|
|
25
|
+
}
|
package/dist/csv/toCsv.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { type CsvFormulaOptions } from "./neutralizeCsvFormula.js";
|
|
1
2
|
/**
|
|
2
3
|
* Serializes rows to CSV. Every cell is wrapped in double quotes (any internal
|
|
3
4
|
* double quote doubled), so delimiters and line breaks inside a cell are safe.
|
|
5
|
+
* A text cell that would run as a spreadsheet formula (`=`, `+`, `-`, `@`…) is
|
|
6
|
+
* prefixed with `'` (see `neutralizeCsvFormula`); number cells never are.
|
|
4
7
|
* Rows are joined with `\n`.
|
|
5
8
|
*
|
|
6
9
|
* @param rows - The rows to serialize, header row included if any.
|
|
7
10
|
* @param delimiter - The cell separator. Defaults to `","`.
|
|
11
|
+
* @param options - `{ neutralizeFormulas }`, `true` by default.
|
|
8
12
|
* @returns The CSV content.
|
|
9
13
|
* @example
|
|
10
14
|
* toCsv([["name", "age"], ["Ann", 32]]); // => '"name","age"\n"Ann","32"'
|
|
11
15
|
* toCsv([['say "hi"']]); // => '"say ""hi"""'
|
|
16
|
+
* toCsv([["=1+1", -3]]); // => `"'=1+1","-3"`
|
|
12
17
|
*/
|
|
13
|
-
export declare function toCsv(rows: readonly (readonly (string | number)[])[], delimiter?: string): string;
|
|
18
|
+
export declare function toCsv(rows: readonly (readonly (string | number)[])[], delimiter?: string, { neutralizeFormulas }?: CsvFormulaOptions): string;
|
|
14
19
|
//# sourceMappingURL=toCsv.d.ts.map
|
package/dist/csv/toCsv.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toCsv.d.ts","sourceRoot":"","sources":["../../src/csv/toCsv.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"toCsv.d.ts","sourceRoot":"","sources":["../../src/csv/toCsv.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EAEvB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CACnB,IAAI,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,EAAE,EAC/C,SAAS,SAAM,EACf,EAAE,kBAAyB,EAAE,GAAE,iBAAsB,GACpD,MAAM,CAUR"}
|
package/dist/csv/toCsv.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
import { neutralizeCsvFormula, } from "./neutralizeCsvFormula.js";
|
|
1
2
|
/**
|
|
2
3
|
* Serializes rows to CSV. Every cell is wrapped in double quotes (any internal
|
|
3
4
|
* double quote doubled), so delimiters and line breaks inside a cell are safe.
|
|
5
|
+
* A text cell that would run as a spreadsheet formula (`=`, `+`, `-`, `@`…) is
|
|
6
|
+
* prefixed with `'` (see `neutralizeCsvFormula`); number cells never are.
|
|
4
7
|
* Rows are joined with `\n`.
|
|
5
8
|
*
|
|
6
9
|
* @param rows - The rows to serialize, header row included if any.
|
|
7
10
|
* @param delimiter - The cell separator. Defaults to `","`.
|
|
11
|
+
* @param options - `{ neutralizeFormulas }`, `true` by default.
|
|
8
12
|
* @returns The CSV content.
|
|
9
13
|
* @example
|
|
10
14
|
* toCsv([["name", "age"], ["Ann", 32]]); // => '"name","age"\n"Ann","32"'
|
|
11
15
|
* toCsv([['say "hi"']]); // => '"say ""hi"""'
|
|
16
|
+
* toCsv([["=1+1", -3]]); // => `"'=1+1","-3"`
|
|
12
17
|
*/
|
|
13
|
-
export function toCsv(rows, delimiter = ",") {
|
|
18
|
+
export function toCsv(rows, delimiter = ",", { neutralizeFormulas = true } = {}) {
|
|
19
|
+
const text = (cell) => typeof cell === "string" && neutralizeFormulas
|
|
20
|
+
? neutralizeCsvFormula(cell)
|
|
21
|
+
: String(cell);
|
|
14
22
|
return rows
|
|
15
|
-
.map((row) => row
|
|
16
|
-
.map((cell) => `"${String(cell).replace(/"/g, '""')}"`)
|
|
17
|
-
.join(delimiter))
|
|
23
|
+
.map((row) => row.map((cell) => `"${text(cell).replace(/"/g, '""')}"`).join(delimiter))
|
|
18
24
|
.join("\n");
|
|
19
25
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Formats the difference between two dates as a human-readable relative
|
|
3
3
|
* string ("3 hours ago", "in 2 days"), backed by `Intl.RelativeTimeFormat`.
|
|
4
|
+
* Where the engine lacks it (Hermes on Android), a built-in fallback covers
|
|
5
|
+
* French and English, and formats any other locale in English.
|
|
4
6
|
*
|
|
5
7
|
* @param date - The date to describe.
|
|
6
8
|
* @param baseDate - The reference date to compare against (defaults to now).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatRelativeTime.d.ts","sourceRoot":"","sources":["../../src/date/formatRelativeTime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"formatRelativeTime.d.ts","sourceRoot":"","sources":["../../src/date/formatRelativeTime.ts"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,QAAQ,GAAE,IAAiB,EAC3B,MAAM,SAAO,GACZ,MAAM,CAmBR"}
|
|
@@ -7,9 +7,40 @@ const UNITS = [
|
|
|
7
7
|
{ unit: "minute", ms: 60 * 1000 },
|
|
8
8
|
{ unit: "second", ms: 1000 },
|
|
9
9
|
];
|
|
10
|
+
const FRENCH_UNITS = {
|
|
11
|
+
year: ["an", "ans"],
|
|
12
|
+
month: ["mois", "mois"],
|
|
13
|
+
week: ["semaine", "semaines"],
|
|
14
|
+
day: ["jour", "jours"],
|
|
15
|
+
hour: ["heure", "heures"],
|
|
16
|
+
minute: ["minute", "minutes"],
|
|
17
|
+
second: ["seconde", "secondes"],
|
|
18
|
+
};
|
|
19
|
+
// Hermes (React Native on Android) ships no `Intl.RelativeTimeFormat`: this
|
|
20
|
+
// covers French and English (any other locale gets English).
|
|
21
|
+
function formatWithoutIntl(value, unit, locale) {
|
|
22
|
+
const abs = Math.abs(value);
|
|
23
|
+
if (locale.split("-")[0]?.toLowerCase() === "fr") {
|
|
24
|
+
if (value === 0)
|
|
25
|
+
return "maintenant";
|
|
26
|
+
if (unit === "day" && abs === 1)
|
|
27
|
+
return value < 0 ? "hier" : "demain";
|
|
28
|
+
const [one, many] = FRENCH_UNITS[unit];
|
|
29
|
+
const text = `${abs} ${abs < 2 ? one : many}`;
|
|
30
|
+
return value < 0 ? `il y a ${text}` : `dans ${text}`;
|
|
31
|
+
}
|
|
32
|
+
if (value === 0)
|
|
33
|
+
return "now";
|
|
34
|
+
if (unit === "day" && abs === 1)
|
|
35
|
+
return value < 0 ? "yesterday" : "tomorrow";
|
|
36
|
+
const text = `${abs} ${unit}${abs === 1 ? "" : "s"}`;
|
|
37
|
+
return value < 0 ? `${text} ago` : `in ${text}`;
|
|
38
|
+
}
|
|
10
39
|
/**
|
|
11
40
|
* Formats the difference between two dates as a human-readable relative
|
|
12
41
|
* string ("3 hours ago", "in 2 days"), backed by `Intl.RelativeTimeFormat`.
|
|
42
|
+
* Where the engine lacks it (Hermes on Android), a built-in fallback covers
|
|
43
|
+
* French and English, and formats any other locale in English.
|
|
13
44
|
*
|
|
14
45
|
* @param date - The date to describe.
|
|
15
46
|
* @param baseDate - The reference date to compare against (defaults to now).
|
|
@@ -20,11 +51,14 @@ const UNITS = [
|
|
|
20
51
|
*/
|
|
21
52
|
export function formatRelativeTime(date, baseDate = new Date(), locale = "en") {
|
|
22
53
|
const diffMs = date.getTime() - baseDate.getTime();
|
|
23
|
-
const
|
|
54
|
+
const format = typeof Intl !== "undefined" && typeof Intl.RelativeTimeFormat === "function"
|
|
55
|
+
? (value, unit) => new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(value, unit)
|
|
56
|
+
: (value, unit) => formatWithoutIntl(value, unit, locale);
|
|
24
57
|
for (const { unit, ms } of UNITS) {
|
|
25
58
|
if (Math.abs(diffMs) >= ms || unit === "second") {
|
|
26
|
-
|
|
59
|
+
// `+ 0` turns a rounded -0 into 0.
|
|
60
|
+
return format(Math.round(diffMs / ms) + 0, unit);
|
|
27
61
|
}
|
|
28
62
|
}
|
|
29
|
-
return
|
|
63
|
+
return format(0, "second");
|
|
30
64
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parseChangelogNotes.d.ts","sourceRoot":"","sources":["../../src/markdown/parseChangelogNotes.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"parseChangelogNotes.d.ts","sourceRoot":"","sources":["../../src/markdown/parseChangelogNotes.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC;AA0BnD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAqBnE"}
|
|
@@ -23,14 +23,21 @@ function parseInlineSegments(text) {
|
|
|
23
23
|
}
|
|
24
24
|
return segments;
|
|
25
25
|
}
|
|
26
|
+
// Any ATX heading level ("# " to "###### ") — release notes are not always
|
|
27
|
+
// written with "###" (e.g. "## What's new" on a hand-written GitHub release).
|
|
28
|
+
const headingPattern = /^#{1,6}\s+/;
|
|
26
29
|
export function parseChangelogNotes(notes) {
|
|
27
30
|
return notes
|
|
28
31
|
.split("\n")
|
|
29
32
|
.map((line) => line.trim())
|
|
30
33
|
.filter((line) => line.length > 0)
|
|
31
34
|
.map((line) => {
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const heading = headingPattern.exec(line);
|
|
36
|
+
if (heading) {
|
|
37
|
+
return {
|
|
38
|
+
type: "heading",
|
|
39
|
+
text: line.slice(heading[0].length).trim(),
|
|
40
|
+
};
|
|
34
41
|
}
|
|
35
42
|
if (line.startsWith("- ")) {
|
|
36
43
|
return {
|
|
@@ -9,7 +9,9 @@ export interface PluralizeOptions {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* Pluralizes a word based on a count. Which counts take the singular follows
|
|
12
|
-
* the locale's plural rules (`Intl.PluralRules`)
|
|
12
|
+
* the locale's plural rules (`Intl.PluralRules`); where the engine lacks it
|
|
13
|
+
* (Hermes on Android), French keeps the singular below 2 and other languages
|
|
14
|
+
* only for ±1. Without an explicit plural
|
|
13
15
|
* form, the default suffix is naive: in English `s`, `y` → `ies`, `es` after
|
|
14
16
|
* `s`/`x`/`z`/`sh`/`ch`; in French `s`, nothing after `s`/`x`/`z`; `s` in
|
|
15
17
|
* any other language.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pluralize.d.ts","sourceRoot":"","sources":["../../src/string/pluralize.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"pluralize.d.ts","sourceRoot":"","sources":["../../src/string/pluralize.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAwCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,GAClC,MAAM,CAKR;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,IACpC,OAAO,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,MAAM,KAAG,MAAM,CAElE"}
|
package/dist/string/pluralize.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
const pluralRules = new Map();
|
|
2
|
+
function languageOf(locale) {
|
|
3
|
+
return locale.split("-")[0]?.toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
// Hermes (React Native on Android) ships no `Intl.PluralRules`: without it,
|
|
6
|
+
// French keeps the singular below 2, every other language only for ±1.
|
|
7
|
+
function fallbackTakesSingular(count, locale) {
|
|
8
|
+
return languageOf(locale) === "fr"
|
|
9
|
+
? Math.abs(count) < 2
|
|
10
|
+
: Math.abs(count) === 1;
|
|
11
|
+
}
|
|
2
12
|
function takesSingular(count, locale) {
|
|
13
|
+
if (typeof Intl === "undefined" || typeof Intl.PluralRules !== "function") {
|
|
14
|
+
return fallbackTakesSingular(count, locale);
|
|
15
|
+
}
|
|
3
16
|
let rules = pluralRules.get(locale);
|
|
4
17
|
if (!rules) {
|
|
5
18
|
rules = new Intl.PluralRules(locale);
|
|
@@ -8,7 +21,7 @@ function takesSingular(count, locale) {
|
|
|
8
21
|
return rules.select(count) === "one";
|
|
9
22
|
}
|
|
10
23
|
function defaultPlural(singular, locale) {
|
|
11
|
-
const language = locale
|
|
24
|
+
const language = languageOf(locale);
|
|
12
25
|
if (language === "fr") {
|
|
13
26
|
return /[sxz]$/i.test(singular) ? singular : `${singular}s`;
|
|
14
27
|
}
|
|
@@ -22,7 +35,9 @@ function defaultPlural(singular, locale) {
|
|
|
22
35
|
}
|
|
23
36
|
/**
|
|
24
37
|
* Pluralizes a word based on a count. Which counts take the singular follows
|
|
25
|
-
* the locale's plural rules (`Intl.PluralRules`)
|
|
38
|
+
* the locale's plural rules (`Intl.PluralRules`); where the engine lacks it
|
|
39
|
+
* (Hermes on Android), French keeps the singular below 2 and other languages
|
|
40
|
+
* only for ±1. Without an explicit plural
|
|
26
41
|
* form, the default suffix is naive: in English `s`, `y` → `ies`, `es` after
|
|
27
42
|
* `s`/`x`/`z`/`sh`/`ch`; in French `s`, nothing after `s`/`x`/`z`; `s` in
|
|
28
43
|
* any other language.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forthtilliath/ts-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"jsdom": "^30.1.1",
|
|
28
28
|
"typescript": "^5.9.3",
|
|
29
29
|
"vitest": "^5.0.2",
|
|
30
|
-
"@forthtilliath/
|
|
31
|
-
"@forthtilliath/
|
|
30
|
+
"@forthtilliath/typescript-config": "0.1.1",
|
|
31
|
+
"@forthtilliath/eslint-config": "0.9.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"dev": "tsc --watch -p tsconfig.build.json",
|