@forthtilliath/ts-kit 0.3.0 → 0.5.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 -2
- package/dist/array/getMostRecentIds.d.ts +21 -0
- package/dist/array/getMostRecentIds.d.ts.map +1 -0
- package/dist/array/getMostRecentIds.js +30 -0
- package/dist/array/index.d.ts +3 -0
- package/dist/array/index.d.ts.map +1 -1
- package/dist/array/index.js +3 -0
- package/dist/array/nextInCycle.d.ts +15 -0
- package/dist/array/nextInCycle.d.ts.map +1 -0
- package/dist/array/nextInCycle.js +19 -0
- package/dist/array/rankByNameMatch.d.ts +19 -0
- package/dist/array/rankByNameMatch.d.ts.map +1 -0
- package/dist/array/rankByNameMatch.js +45 -0
- package/dist/date/getPeriodStartMs.d.ts +16 -0
- package/dist/date/getPeriodStartMs.d.ts.map +1 -0
- package/dist/date/getPeriodStartMs.js +24 -0
- package/dist/date/index.d.ts +1 -0
- package/dist/date/index.d.ts.map +1 -1
- package/dist/date/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/markdown/index.d.ts +2 -0
- package/dist/markdown/index.d.ts.map +1 -0
- package/dist/markdown/index.js +1 -0
- package/dist/markdown/parseChangelogNotes.d.ts +16 -0
- package/dist/markdown/parseChangelogNotes.d.ts.map +1 -0
- package/dist/markdown/parseChangelogNotes.js +43 -0
- package/dist/number/formatCsvNumber.d.ts +14 -0
- package/dist/number/formatCsvNumber.d.ts.map +1 -0
- package/dist/number/formatCsvNumber.js +15 -0
- package/dist/number/index.d.ts +1 -0
- package/dist/number/index.d.ts.map +1 -1
- package/dist/number/index.js +1 -0
- package/dist/string/escapeCsvField.d.ts +14 -0
- package/dist/string/escapeCsvField.d.ts.map +1 -0
- package/dist/string/escapeCsvField.js +17 -0
- package/dist/string/escapeHtml.d.ts +12 -0
- package/dist/string/escapeHtml.d.ts.map +1 -0
- package/dist/string/escapeHtml.js +17 -0
- package/dist/string/index.d.ts +3 -0
- package/dist/string/index.d.ts.map +1 -1
- package/dist/string/index.js +3 -0
- package/dist/string/normalizeForSearch.d.ts +12 -0
- package/dist/string/normalizeForSearch.d.ts.map +1 -0
- package/dist/string/normalizeForSearch.js +25 -0
- package/dist/version/compareVersions.d.ts +7 -0
- package/dist/version/compareVersions.d.ts.map +1 -0
- package/dist/version/compareVersions.js +17 -0
- package/dist/version/index.d.ts +2 -0
- package/dist/version/index.d.ts.map +1 -0
- package/dist/version/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @forthtilliath/ts-kit
|
|
2
2
|
|
|
3
3
|
Framework-agnostic TypeScript utility functions — arrays, objects, strings,
|
|
4
|
-
async control-flow, dates, numbers, browser file downloads,
|
|
5
|
-
`Array` extensions with better type-narrowing.
|
|
4
|
+
async control-flow, dates, numbers, browser file downloads, changelog/version
|
|
5
|
+
parsing, and a couple of `Array` extensions with better type-narrowing.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -37,6 +37,15 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
37
37
|
|
|
38
38
|
- `chunk(array, size)` — splits an array into chunks of a given size.
|
|
39
39
|
- `flattenDeep(array)` — recursively flattens nested arrays.
|
|
40
|
+
- `getMostRecentIds(rows, limit?)` — most recently occurring distinct ids
|
|
41
|
+
(`{ id, occurredAt }[]`), most recent first, deduplicated, limited
|
|
42
|
+
(default 5).
|
|
43
|
+
- `nextInCycle(ids, currentId)` — next id in a short list (e.g. cycling
|
|
44
|
+
through recents on tap) — wraps to the first if `currentId` is at the end
|
|
45
|
+
or no longer in the list.
|
|
46
|
+
- `rankByNameMatch(items, query, getName)` — ranks `items` by relevance to
|
|
47
|
+
`query`: earlier match position first, then shorter name — for a
|
|
48
|
+
search-as-you-type list.
|
|
40
49
|
|
|
41
50
|
### `async`
|
|
42
51
|
|
|
@@ -78,6 +87,9 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
78
87
|
- `isSameDay(a, b)` — checks whether two dates fall on the same local day.
|
|
79
88
|
- `formatRelativeTime(date, baseDate?, locale?)` — human-readable relative
|
|
80
89
|
time ("3 hours ago"), backed by `Intl.RelativeTimeFormat`.
|
|
90
|
+
- `getPeriodStartMs(period, now?)` — start timestamp (ms) for a
|
|
91
|
+
`"today" | "7d" | "30d" | "all"` period filter, or `null` for `"all"`.
|
|
92
|
+
`"today"` is the current calendar day, not a rolling 24h window.
|
|
81
93
|
|
|
82
94
|
### `files`
|
|
83
95
|
|
|
@@ -91,6 +103,14 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
91
103
|
- `randomId()` — `crypto.randomUUID()` with fallbacks for older/insecure
|
|
92
104
|
contexts.
|
|
93
105
|
|
|
106
|
+
### `markdown`
|
|
107
|
+
|
|
108
|
+
- `parseChangelogNotes(notes)` — parses a small subset of Markdown
|
|
109
|
+
(`### heading`, `- item`, `**bold**`) commonly found in GitHub release
|
|
110
|
+
notes into a list of typed blocks (`heading` / `item` / `text`, each with
|
|
111
|
+
`bold`-aware segments), ready for a UI layer to render without a full
|
|
112
|
+
Markdown dependency.
|
|
113
|
+
|
|
94
114
|
### `maths`
|
|
95
115
|
|
|
96
116
|
- `sum(numbers)` — sum of an array of numbers.
|
|
@@ -102,6 +122,8 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
102
122
|
- `round(value, decimals?)` — rounds to a given number of decimal places.
|
|
103
123
|
- `formatBytes(bytes, decimals?)` — human-readable byte size ("1.5 MB").
|
|
104
124
|
- `formatDuration(ms)` — human-readable duration ("1d 1h 1m 1s").
|
|
125
|
+
- `formatCsvNumber(value, decimals?)` — formats a number with a comma
|
|
126
|
+
decimal separator (French-locale spreadsheets) instead of JS's dot.
|
|
105
127
|
|
|
106
128
|
### `object`
|
|
107
129
|
|
|
@@ -122,6 +144,18 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
122
144
|
hyphenated).
|
|
123
145
|
- `pluralize(count, singular, plural?)` — naive English pluralization, with an
|
|
124
146
|
override for irregular words.
|
|
147
|
+
- `normalizeForSearch(text)` — lowercases, trims, strips accents, and expands
|
|
148
|
+
œ/æ ligatures (which `normalize("NFD")` alone doesn't decompose) — for
|
|
149
|
+
accent/case-insensitive search matching.
|
|
150
|
+
- `escapeCsvField(value)` — quotes and escapes a CSV field (RFC 4180) only if
|
|
151
|
+
it contains a `"`, `;`, or newline.
|
|
152
|
+
- `escapeHtml(text)` — basic HTML entity escaping (`&`, `<`, `>`, `"`) for
|
|
153
|
+
inserting user text into an HTML template.
|
|
154
|
+
|
|
155
|
+
### `version`
|
|
156
|
+
|
|
157
|
+
- `compareVersions(a, b)` — compares two `"x.y.z"` version strings, segment
|
|
158
|
+
by segment; `-1`/`0`/`1`.
|
|
125
159
|
|
|
126
160
|
## Scripts
|
|
127
161
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RecentIdRow {
|
|
2
|
+
id: number | null;
|
|
3
|
+
occurredAt: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Returns the most recently occurring distinct ids, most recent first —
|
|
7
|
+
* useful for a "recents" picker (a food, a container, or any other dated
|
|
8
|
+
* entity). Rows with a `null` id (a reference deleted since) are ignored:
|
|
9
|
+
* there's nothing left to offer for them.
|
|
10
|
+
*
|
|
11
|
+
* @param rows - Rows carrying an id (or `null`) and an ISO/parsable date.
|
|
12
|
+
* @param limit - Max number of ids to return. Defaults to `5`.
|
|
13
|
+
* @returns Distinct ids, most recent first, limited to `limit`.
|
|
14
|
+
* @example
|
|
15
|
+
* getMostRecentIds([
|
|
16
|
+
* { id: 1, occurredAt: "2026-01-01T00:00:00.000Z" },
|
|
17
|
+
* { id: 2, occurredAt: "2026-01-03T00:00:00.000Z" },
|
|
18
|
+
* ]); // => [2, 1]
|
|
19
|
+
*/
|
|
20
|
+
export declare function getMostRecentIds(rows: RecentIdRow[], limit?: number): number[];
|
|
21
|
+
//# sourceMappingURL=getMostRecentIds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMostRecentIds.d.ts","sourceRoot":"","sources":["../../src/array/getMostRecentIds.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EAAE,EACnB,KAAK,SAAgB,GACpB,MAAM,EAAE,CAcV"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 5;
|
|
2
|
+
/**
|
|
3
|
+
* Returns the most recently occurring distinct ids, most recent first —
|
|
4
|
+
* useful for a "recents" picker (a food, a container, or any other dated
|
|
5
|
+
* entity). Rows with a `null` id (a reference deleted since) are ignored:
|
|
6
|
+
* there's nothing left to offer for them.
|
|
7
|
+
*
|
|
8
|
+
* @param rows - Rows carrying an id (or `null`) and an ISO/parsable date.
|
|
9
|
+
* @param limit - Max number of ids to return. Defaults to `5`.
|
|
10
|
+
* @returns Distinct ids, most recent first, limited to `limit`.
|
|
11
|
+
* @example
|
|
12
|
+
* getMostRecentIds([
|
|
13
|
+
* { id: 1, occurredAt: "2026-01-01T00:00:00.000Z" },
|
|
14
|
+
* { id: 2, occurredAt: "2026-01-03T00:00:00.000Z" },
|
|
15
|
+
* ]); // => [2, 1]
|
|
16
|
+
*/
|
|
17
|
+
export function getMostRecentIds(rows, limit = DEFAULT_LIMIT) {
|
|
18
|
+
const sorted = [...rows].sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const result = [];
|
|
21
|
+
for (const row of sorted) {
|
|
22
|
+
if (row.id == null || seen.has(row.id))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(row.id);
|
|
25
|
+
result.push(row.id);
|
|
26
|
+
if (result.length >= limit)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
package/dist/array/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
|
package/dist/array/index.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the next element in a short list (e.g. cycling through a few
|
|
3
|
+
* recent items on tap). Wraps to the first element if the current one is no
|
|
4
|
+
* longer in the list (deleted, or out of range) or was the last one.
|
|
5
|
+
*
|
|
6
|
+
* @param ids - The list to cycle through.
|
|
7
|
+
* @param currentId - The currently selected id, or `null` for none.
|
|
8
|
+
* @returns The next id, or `null` if `ids` is empty.
|
|
9
|
+
* @example
|
|
10
|
+
* nextInCycle([1, 2, 3], 1); // => 2
|
|
11
|
+
* nextInCycle([1, 2, 3], 3); // => 1
|
|
12
|
+
* nextInCycle([1, 2, 3], null); // => 1
|
|
13
|
+
*/
|
|
14
|
+
export declare function nextInCycle(ids: number[], currentId: number | null): number | null;
|
|
15
|
+
//# sourceMappingURL=nextInCycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextInCycle.d.ts","sourceRoot":"","sources":["../../src/array/nextInCycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EAAE,EACb,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,MAAM,GAAG,IAAI,CAIf"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the next element in a short list (e.g. cycling through a few
|
|
3
|
+
* recent items on tap). Wraps to the first element if the current one is no
|
|
4
|
+
* longer in the list (deleted, or out of range) or was the last one.
|
|
5
|
+
*
|
|
6
|
+
* @param ids - The list to cycle through.
|
|
7
|
+
* @param currentId - The currently selected id, or `null` for none.
|
|
8
|
+
* @returns The next id, or `null` if `ids` is empty.
|
|
9
|
+
* @example
|
|
10
|
+
* nextInCycle([1, 2, 3], 1); // => 2
|
|
11
|
+
* nextInCycle([1, 2, 3], 3); // => 1
|
|
12
|
+
* nextInCycle([1, 2, 3], null); // => 1
|
|
13
|
+
*/
|
|
14
|
+
export function nextInCycle(ids, currentId) {
|
|
15
|
+
if (ids.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const currentIndex = currentId == null ? -1 : ids.indexOf(currentId);
|
|
18
|
+
return ids[(currentIndex + 1) % ids.length] ?? null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranks `items` by relevance to a text search: match position in the name
|
|
3
|
+
* (earlier is more relevant), then name length. Generic so it can be reused
|
|
4
|
+
* for any list of items with a displayable name — typically for a
|
|
5
|
+
* search-as-you-type list.
|
|
6
|
+
*
|
|
7
|
+
* @param items - The items to rank.
|
|
8
|
+
* @param query - The search text.
|
|
9
|
+
* @param getName - Reads the displayable name off an item.
|
|
10
|
+
* @returns Only the matching items, ranked most relevant first.
|
|
11
|
+
* @example
|
|
12
|
+
* rankByNameMatch(
|
|
13
|
+
* [{ label: "Salad with potato" }, { label: "Potato" }],
|
|
14
|
+
* "potato",
|
|
15
|
+
* (item) => item.label,
|
|
16
|
+
* ); // => [{ label: "Potato" }, { label: "Salad with potato" }]
|
|
17
|
+
*/
|
|
18
|
+
export declare function rankByNameMatch<T>(items: T[], query: string, getName: (item: T) => string): T[];
|
|
19
|
+
//# sourceMappingURL=rankByNameMatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rankByNameMatch.d.ts","sourceRoot":"","sources":["../../src/array/rankByNameMatch.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAC3B,CAAC,EAAE,CAiBL"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { normalizeForSearch } from "../string/normalizeForSearch.js";
|
|
2
|
+
// Caches each item's normalized name by object reference: useful for fixed
|
|
3
|
+
// lists (module-level constants) searched on every keystroke in a search
|
|
4
|
+
// field, to avoid renormalizing every name on each call.
|
|
5
|
+
const normalizedNameCache = new WeakMap();
|
|
6
|
+
function getNormalizedName(item, getName) {
|
|
7
|
+
if (typeof item !== "object" || item === null)
|
|
8
|
+
return normalizeForSearch(getName(item));
|
|
9
|
+
const cached = normalizedNameCache.get(item);
|
|
10
|
+
if (cached !== undefined)
|
|
11
|
+
return cached;
|
|
12
|
+
const normalized = normalizeForSearch(getName(item));
|
|
13
|
+
normalizedNameCache.set(item, normalized);
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ranks `items` by relevance to a text search: match position in the name
|
|
18
|
+
* (earlier is more relevant), then name length. Generic so it can be reused
|
|
19
|
+
* for any list of items with a displayable name — typically for a
|
|
20
|
+
* search-as-you-type list.
|
|
21
|
+
*
|
|
22
|
+
* @param items - The items to rank.
|
|
23
|
+
* @param query - The search text.
|
|
24
|
+
* @param getName - Reads the displayable name off an item.
|
|
25
|
+
* @returns Only the matching items, ranked most relevant first.
|
|
26
|
+
* @example
|
|
27
|
+
* rankByNameMatch(
|
|
28
|
+
* [{ label: "Salad with potato" }, { label: "Potato" }],
|
|
29
|
+
* "potato",
|
|
30
|
+
* (item) => item.label,
|
|
31
|
+
* ); // => [{ label: "Potato" }, { label: "Salad with potato" }]
|
|
32
|
+
*/
|
|
33
|
+
export function rankByNameMatch(items, query, getName) {
|
|
34
|
+
const normalizedQuery = normalizeForSearch(query);
|
|
35
|
+
const matches = [];
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const matchIndex = getNormalizedName(item, getName).indexOf(normalizedQuery);
|
|
38
|
+
if (matchIndex !== -1) {
|
|
39
|
+
matches.push({ item, matchIndex });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
matches.sort((a, b) => a.matchIndex - b.matchIndex ||
|
|
43
|
+
getName(a.item).length - getName(b.item).length);
|
|
44
|
+
return matches.map((m) => m.item);
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type PeriodFilter = "all" | "today" | "7d" | "30d";
|
|
2
|
+
/**
|
|
3
|
+
* Returns the timestamp (ms) from which a record is included for a given
|
|
4
|
+
* period filter, or `null` for `"all"` (no date filter). `"today"` starts at
|
|
5
|
+
* the current calendar day, not a rolling 24h window.
|
|
6
|
+
*
|
|
7
|
+
* @param period - The period filter.
|
|
8
|
+
* @param now - The reference date. Defaults to `new Date()`.
|
|
9
|
+
* @returns The period's start timestamp in ms, or `null` for `"all"`.
|
|
10
|
+
* @example
|
|
11
|
+
* getPeriodStartMs("today"); // => start of today, local time, in ms
|
|
12
|
+
* getPeriodStartMs("7d"); // => now minus 7 days, in ms
|
|
13
|
+
* getPeriodStartMs("all"); // => null
|
|
14
|
+
*/
|
|
15
|
+
export declare function getPeriodStartMs(period: PeriodFilter, now?: Date): number | null;
|
|
16
|
+
//# sourceMappingURL=getPeriodStartMs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getPeriodStartMs.d.ts","sourceRoot":"","sources":["../../src/date/getPeriodStartMs.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,YAAY,EACpB,GAAG,OAAa,GACf,MAAM,GAAG,IAAI,CASf"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the timestamp (ms) from which a record is included for a given
|
|
3
|
+
* period filter, or `null` for `"all"` (no date filter). `"today"` starts at
|
|
4
|
+
* the current calendar day, not a rolling 24h window.
|
|
5
|
+
*
|
|
6
|
+
* @param period - The period filter.
|
|
7
|
+
* @param now - The reference date. Defaults to `new Date()`.
|
|
8
|
+
* @returns The period's start timestamp in ms, or `null` for `"all"`.
|
|
9
|
+
* @example
|
|
10
|
+
* getPeriodStartMs("today"); // => start of today, local time, in ms
|
|
11
|
+
* getPeriodStartMs("7d"); // => now minus 7 days, in ms
|
|
12
|
+
* getPeriodStartMs("all"); // => null
|
|
13
|
+
*/
|
|
14
|
+
export function getPeriodStartMs(period, now = new Date()) {
|
|
15
|
+
if (period === "all")
|
|
16
|
+
return null;
|
|
17
|
+
if (period === "today") {
|
|
18
|
+
const startOfDay = new Date(now);
|
|
19
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
20
|
+
return startOfDay.getTime();
|
|
21
|
+
}
|
|
22
|
+
const days = period === "7d" ? 7 : 30;
|
|
23
|
+
return now.getTime() - days * 24 * 60 * 60 * 1000;
|
|
24
|
+
}
|
package/dist/date/index.d.ts
CHANGED
package/dist/date/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/date/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/date/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
package/dist/date/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export * from "./classes/index.js";
|
|
|
4
4
|
export * from "./date/index.js";
|
|
5
5
|
export * from "./files/index.js";
|
|
6
6
|
export * from "./id/index.js";
|
|
7
|
+
export * from "./markdown/index.js";
|
|
7
8
|
export * from "./maths/index.js";
|
|
8
9
|
export * from "./number/index.js";
|
|
9
10
|
export * from "./object/index.js";
|
|
10
11
|
export * from "./string/index.js";
|
|
12
|
+
export * from "./version/index.js";
|
|
11
13
|
//# sourceMappingURL=index.d.ts.map
|
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,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,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,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,9 @@ export * from "./classes/index.js";
|
|
|
9
9
|
export * from "./date/index.js";
|
|
10
10
|
export * from "./files/index.js";
|
|
11
11
|
export * from "./id/index.js";
|
|
12
|
+
export * from "./markdown/index.js";
|
|
12
13
|
export * from "./maths/index.js";
|
|
13
14
|
export * from "./number/index.js";
|
|
14
15
|
export * from "./object/index.js";
|
|
15
16
|
export * from "./string/index.js";
|
|
17
|
+
export * from "./version/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/markdown/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./parseChangelogNotes.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ChangelogSegment {
|
|
2
|
+
text: string;
|
|
3
|
+
bold: boolean;
|
|
4
|
+
}
|
|
5
|
+
export type ChangelogBlock = {
|
|
6
|
+
type: "heading";
|
|
7
|
+
text: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "item";
|
|
10
|
+
segments: ChangelogSegment[];
|
|
11
|
+
} | {
|
|
12
|
+
type: "text";
|
|
13
|
+
segments: ChangelogSegment[];
|
|
14
|
+
};
|
|
15
|
+
export declare function parseChangelogNotes(notes: string): ChangelogBlock[];
|
|
16
|
+
//# sourceMappingURL=parseChangelogNotes.d.ts.map
|
|
@@ -0,0 +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;AAsBnD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAiBnE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// GitHub release notes (and changelogs in general) are usually written in a
|
|
2
|
+
// small subset of Markdown ("### Heading", "- item", "**bold**"). Rendered
|
|
3
|
+
// as-is in a plain UI text node, the raw syntax shows up unstyled — this
|
|
4
|
+
// parser turns it into a small set of typed blocks a UI layer can render
|
|
5
|
+
// (heading / bullet item / plain text), without pulling in a full Markdown
|
|
6
|
+
// dependency for this narrow use case.
|
|
7
|
+
function parseInlineSegments(text) {
|
|
8
|
+
const segments = [];
|
|
9
|
+
const boldPattern = /\*\*(.+?)\*\*/g;
|
|
10
|
+
let lastIndex = 0;
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = boldPattern.exec(text)) !== null) {
|
|
13
|
+
if (match.index > lastIndex) {
|
|
14
|
+
segments.push({ text: text.slice(lastIndex, match.index), bold: false });
|
|
15
|
+
}
|
|
16
|
+
// The capture group is mandatory in the pattern, so it always matches —
|
|
17
|
+
// the `?? ""` fallback is only here to satisfy `noUncheckedIndexedAccess`.
|
|
18
|
+
segments.push({ text: match[1] ?? "", bold: true });
|
|
19
|
+
lastIndex = boldPattern.lastIndex;
|
|
20
|
+
}
|
|
21
|
+
if (lastIndex < text.length) {
|
|
22
|
+
segments.push({ text: text.slice(lastIndex), bold: false });
|
|
23
|
+
}
|
|
24
|
+
return segments;
|
|
25
|
+
}
|
|
26
|
+
export function parseChangelogNotes(notes) {
|
|
27
|
+
return notes
|
|
28
|
+
.split("\n")
|
|
29
|
+
.map((line) => line.trim())
|
|
30
|
+
.filter((line) => line.length > 0)
|
|
31
|
+
.map((line) => {
|
|
32
|
+
if (line.startsWith("### ")) {
|
|
33
|
+
return { type: "heading", text: line.slice(4).trim() };
|
|
34
|
+
}
|
|
35
|
+
if (line.startsWith("- ")) {
|
|
36
|
+
return {
|
|
37
|
+
type: "item",
|
|
38
|
+
segments: parseInlineSegments(line.slice(2).trim()),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { type: "text", segments: parseInlineSegments(line) };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a number as a raw value (no unit suffix) for use in a spreadsheet
|
|
3
|
+
* (sum, average...), with a comma decimal separator as expected by
|
|
4
|
+
* French-locale spreadsheets instead of JS's dot.
|
|
5
|
+
*
|
|
6
|
+
* @param value - The number to format.
|
|
7
|
+
* @param decimals - Number of decimal places. Defaults to `1`.
|
|
8
|
+
* @returns The formatted number, comma as decimal separator.
|
|
9
|
+
* @example
|
|
10
|
+
* formatCsvNumber(1.5); // => "1,5"
|
|
11
|
+
* formatCsvNumber(12.345, 2); // => "12,35"
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatCsvNumber(value: number, decimals?: number): string;
|
|
14
|
+
//# sourceMappingURL=formatCsvNumber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatCsvNumber.d.ts","sourceRoot":"","sources":["../../src/number/formatCsvNumber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAEnE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a number as a raw value (no unit suffix) for use in a spreadsheet
|
|
3
|
+
* (sum, average...), with a comma decimal separator as expected by
|
|
4
|
+
* French-locale spreadsheets instead of JS's dot.
|
|
5
|
+
*
|
|
6
|
+
* @param value - The number to format.
|
|
7
|
+
* @param decimals - Number of decimal places. Defaults to `1`.
|
|
8
|
+
* @returns The formatted number, comma as decimal separator.
|
|
9
|
+
* @example
|
|
10
|
+
* formatCsvNumber(1.5); // => "1,5"
|
|
11
|
+
* formatCsvNumber(12.345, 2); // => "12,35"
|
|
12
|
+
*/
|
|
13
|
+
export function formatCsvNumber(value, decimals = 1) {
|
|
14
|
+
return value.toFixed(decimals).replace(".", ",");
|
|
15
|
+
}
|
package/dist/number/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/number/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/number/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC"}
|
package/dist/number/index.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes a CSV field per RFC 4180: wraps it in double quotes and doubles any
|
|
3
|
+
* internal double quote if the value contains a double quote, a semicolon
|
|
4
|
+
* (the delimiter), or a newline — returned as-is otherwise.
|
|
5
|
+
*
|
|
6
|
+
* @param value - The raw field value.
|
|
7
|
+
* @returns The value, quoted and escaped only if needed.
|
|
8
|
+
* @example
|
|
9
|
+
* escapeCsvField("Apple"); // => "Apple"
|
|
10
|
+
* escapeCsvField("a;b"); // => '"a;b"'
|
|
11
|
+
* escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
|
|
12
|
+
*/
|
|
13
|
+
export declare function escapeCsvField(value: string): string;
|
|
14
|
+
//# sourceMappingURL=escapeCsvField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/string/escapeCsvField.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes a CSV field per RFC 4180: wraps it in double quotes and doubles any
|
|
3
|
+
* internal double quote if the value contains a double quote, a semicolon
|
|
4
|
+
* (the delimiter), or a newline — returned as-is otherwise.
|
|
5
|
+
*
|
|
6
|
+
* @param value - The raw field value.
|
|
7
|
+
* @returns The value, quoted and escaped only if needed.
|
|
8
|
+
* @example
|
|
9
|
+
* escapeCsvField("Apple"); // => "Apple"
|
|
10
|
+
* escapeCsvField("a;b"); // => '"a;b"'
|
|
11
|
+
* escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
|
|
12
|
+
*/
|
|
13
|
+
export function escapeCsvField(value) {
|
|
14
|
+
if (!/[";\n]/.test(value))
|
|
15
|
+
return value;
|
|
16
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes the HTML entities that matter when inserting arbitrary text into
|
|
3
|
+
* an HTML template (`&`, `<`, `>`, `"`), without pulling in a templating
|
|
4
|
+
* dependency.
|
|
5
|
+
*
|
|
6
|
+
* @param text - The text to escape.
|
|
7
|
+
* @returns `text` with `&`, `<`, `>` and `"` replaced by their HTML entities.
|
|
8
|
+
* @example
|
|
9
|
+
* escapeHtml("Tom & Jerry <script>"); // => "Tom & Jerry <script>"
|
|
10
|
+
*/
|
|
11
|
+
export declare function escapeHtml(text: string): string;
|
|
12
|
+
//# sourceMappingURL=escapeHtml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escapeHtml.d.ts","sourceRoot":"","sources":["../../src/string/escapeHtml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes the HTML entities that matter when inserting arbitrary text into
|
|
3
|
+
* an HTML template (`&`, `<`, `>`, `"`), without pulling in a templating
|
|
4
|
+
* dependency.
|
|
5
|
+
*
|
|
6
|
+
* @param text - The text to escape.
|
|
7
|
+
* @returns `text` with `&`, `<`, `>` and `"` replaced by their HTML entities.
|
|
8
|
+
* @example
|
|
9
|
+
* escapeHtml("Tom & Jerry <script>"); // => "Tom & Jerry <script>"
|
|
10
|
+
*/
|
|
11
|
+
export function escapeHtml(text) {
|
|
12
|
+
return text
|
|
13
|
+
.replace(/&/g, "&")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">")
|
|
16
|
+
.replace(/"/g, """);
|
|
17
|
+
}
|
package/dist/string/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/string/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/string/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
package/dist/string/index.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes text for accent/case-insensitive search matching: expands
|
|
3
|
+
* œ/æ ligatures, strips accents, lower-cases, and trims.
|
|
4
|
+
*
|
|
5
|
+
* @param text - The text to normalize.
|
|
6
|
+
* @returns The normalized text.
|
|
7
|
+
* @example
|
|
8
|
+
* normalizeForSearch("Pâtes À La Crème"); // => "pates a la creme"
|
|
9
|
+
* normalizeForSearch("Œuf"); // => "oeuf"
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeForSearch(text: string): string;
|
|
12
|
+
//# sourceMappingURL=normalizeForSearch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizeForSearch.d.ts","sourceRoot":"","sources":["../../src/string/normalizeForSearch.ts"],"names":[],"mappings":"AASA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The œ/æ ligatures do NOT decompose via normalize("NFD") (unlike accented
|
|
2
|
+
// letters): they're standalone Unicode letters, not a base + combining mark.
|
|
3
|
+
// Without this explicit replacement, searching "Œuf" (the real ligature)
|
|
4
|
+
// never matches "Oeuf" in data spelled without the ligature (as most text
|
|
5
|
+
// databases are).
|
|
6
|
+
function expandLigatures(text) {
|
|
7
|
+
return text.replace(/[œŒ]/g, "oe").replace(/[æÆ]/g, "ae");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes text for accent/case-insensitive search matching: expands
|
|
11
|
+
* œ/æ ligatures, strips accents, lower-cases, and trims.
|
|
12
|
+
*
|
|
13
|
+
* @param text - The text to normalize.
|
|
14
|
+
* @returns The normalized text.
|
|
15
|
+
* @example
|
|
16
|
+
* normalizeForSearch("Pâtes À La Crème"); // => "pates a la creme"
|
|
17
|
+
* normalizeForSearch("Œuf"); // => "oeuf"
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeForSearch(text) {
|
|
20
|
+
return expandLigatures(text)
|
|
21
|
+
.normalize("NFD")
|
|
22
|
+
.replace(/[̀-ͯ]/g, "")
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compareVersions.d.ts","sourceRoot":"","sources":["../../src/version/compareVersions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU5D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two "x.y.z" version strings, segment by segment.
|
|
3
|
+
*
|
|
4
|
+
* @returns -1 if `a` < `b`, 0 if they're equal, 1 if `a` > `b`.
|
|
5
|
+
*/
|
|
6
|
+
export function compareVersions(a, b) {
|
|
7
|
+
const partsA = a.split(".").map(Number);
|
|
8
|
+
const partsB = b.split(".").map(Number);
|
|
9
|
+
const length = Math.max(partsA.length, partsB.length);
|
|
10
|
+
for (let i = 0; i < length; i++) {
|
|
11
|
+
const numA = partsA[i] ?? 0;
|
|
12
|
+
const numB = partsB[i] ?? 0;
|
|
13
|
+
if (numA !== numB)
|
|
14
|
+
return numA > numB ? 1 : -1;
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/version/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./compareVersions.js";
|