@stll/text-normalize 0.0.1-placeholder.0 → 0.1.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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Latin/combining-mark diacritic stripping for search keys and slugs.
3
+ *
4
+ * Both variants decompose the input and then remove every character
5
+ * Unicode classifies as a diacritic (`\p{Diacritic}`). `\p{Diacritic}`
6
+ * is deliberate: it covers combining marks in every block, not only the
7
+ * Combining Diacritical Marks block (U+0300–U+036F). A range like
8
+ * `[̀-ͯ]` misses marks in the Extended (U+1AB0–U+1AFF) and
9
+ * Supplement (U+1DC0–U+1DFF) blocks, so two callers using different
10
+ * classes would disagree on the same text.
11
+ *
12
+ * The variants differ only in the decomposition form:
13
+ *
14
+ * - `stripDiacritics` uses NFD (canonical decomposition). Use it for
15
+ * search keys, where compatibility characters must survive unchanged.
16
+ * - `stripDiacriticsForSlug` uses NFKD (compatibility decomposition), so
17
+ * ligatures, full-width forms, superscripts, and similar fold to their
18
+ * ASCII base before the `[a-z0-9]` slug filter runs. Slugs are
19
+ * persisted, public URL segments, so this variant pins the exact form
20
+ * the existing slugs were generated with; do not switch it to NFD.
21
+ */
22
+ const DIACRITIC_RE = /\p{Diacritic}/gu;
23
+ /**
24
+ * Strip combining diacritics from search text (NFD). Non-diacritic
25
+ * characters, including compatibility characters, pass through unchanged.
26
+ */
27
+ export const stripDiacritics = (text) => text.normalize("NFD").replace(DIACRITIC_RE, "");
28
+ /**
29
+ * Strip combining diacritics for slug generation (NFKD). Callers apply
30
+ * their own case folding and `[a-z0-9]` filtering around this; the NFKD
31
+ * form is load-bearing for byte-stable slugs and must not change.
32
+ */
33
+ export const stripDiacriticsForSlug = (text) => text.normalize("NFKD").replace(DIACRITIC_RE, "");
34
+ //# sourceMappingURL=diacritics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diacritics.js","sourceRoot":"","sources":["../src/diacritics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAEvC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAU,EAAE,CACtD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,IAAY,EAAU,EAAE,CAC7D,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The one escaper for text that becomes part of a pattern.
3
+ *
4
+ * Every call site builds a regular expression around something a document or
5
+ * a user wrote, so each needed the same escape and each had written it out.
6
+ * Two spellings of this is one spelling too many: the character class is the
7
+ * contract, and a call site that escapes one character fewer matches things
8
+ * its author never meant it to.
9
+ */
10
+ export declare const escapeRegExp: (value: string) => string;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The one escaper for text that becomes part of a pattern.
3
+ *
4
+ * Every call site builds a regular expression around something a document or
5
+ * a user wrote, so each needed the same escape and each had written it out.
6
+ * Two spellings of this is one spelling too many: the character class is the
7
+ * contract, and a call site that escapes one character fewer matches things
8
+ * its author never meant it to.
9
+ */
10
+ export const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
11
+ //# sourceMappingURL=escape-regexp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape-regexp.js","sourceRoot":"","sources":["../src/escape-regexp.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAU,EAAE,CACpD,KAAK,CAAC,OAAO,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ export { applyArabicFolds, applyArabicFoldsWithOffsets, ARABIC_DIGIT_FOLDS, ARABIC_LETTER_FOLDS, ARABIC_REMOVED, } from "./arabic.js";
2
+ export type { FoldedText } from "./arabic.js";
3
+ export { foldToAscii } from "./ascii-fold.js";
4
+ export { ASCII_FOLD_TABLE } from "./ascii-fold-table.js";
5
+ export { stripDiacritics, stripDiacriticsForSlug } from "./diacritics.js";
6
+ export { escapeRegExp } from "./escape-regexp.js";
7
+ export { arabicNormalize } from "./normalize.js";
8
+ export { findSearchMatchRanges, foldSearchMatchText, foldSearchMatchTextWithOffsets, } from "./search-match.js";
9
+ export type { FoldedSearchText, SearchMatchRange } from "./search-match.js";
10
+ export { collapseSpacedLetters, spacedLetterRunRegex, } from "./spaced-letters.js";
11
+ export { slugify } from "./slug.js";
12
+ export type { SlugCharset, SlugifyOptions, SlugSeparator } from "./slug.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { applyArabicFolds, applyArabicFoldsWithOffsets, ARABIC_DIGIT_FOLDS, ARABIC_LETTER_FOLDS, ARABIC_REMOVED, } from "./arabic.js";
2
+ export { foldToAscii } from "./ascii-fold.js";
3
+ export { ASCII_FOLD_TABLE } from "./ascii-fold-table.js";
4
+ export { stripDiacritics, stripDiacriticsForSlug } from "./diacritics.js";
5
+ export { escapeRegExp } from "./escape-regexp.js";
6
+ export { arabicNormalize } from "./normalize.js";
7
+ export { findSearchMatchRanges, foldSearchMatchText, foldSearchMatchTextWithOffsets, } from "./search-match.js";
8
+ export { collapseSpacedLetters, spacedLetterRunRegex, } from "./spaced-letters.js";
9
+ export { slugify } from "./slug.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,8BAA8B,GAC/B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * TypeScript mirror of the SQL `arabic_normalize()` function. Folds Arabic
3
+ * orthographic variants so a query matches regardless of how the text was
4
+ * typed (alef/hamza/teh-marbuta/yeh variants, tashkeel, tatweel,
5
+ * Arabic-Indic digits), then applies locale-stable ASCII case folding for
6
+ * mixed-script names. It must NOT strip Latin diacritics — the stored match
7
+ * keys don't; client-side diacritic-insensitive matching is
8
+ * `foldSearchMatchText` (search-match.ts).
9
+ *
10
+ * NFKC runs first so presentation forms (U+FB50–FDFF, U+FE70–FEFF) fold
11
+ * to their canonical letters before the explicit folds apply.
12
+ *
13
+ * This MUST stay consistent with the SQL `arabic_normalize()` function;
14
+ * the golden vectors in normalize.test.ts pin the contract for both.
15
+ */
16
+ export declare const arabicNormalize: (text: string) => string;
@@ -0,0 +1,32 @@
1
+ import { applyArabicFolds } from "./arabic.js";
2
+ const ASCII_UPPERCASE_RE = /[A-Zİ]/gu;
3
+ const SEARCH_WHITESPACE_RE = /[ \t\n\v\f\r\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/gu;
4
+ const foldSearchCase = (text) => text.replace(ASCII_UPPERCASE_RE, (char) => {
5
+ if (char === "İ") {
6
+ return "i";
7
+ }
8
+ const codePoint = char.codePointAt(0);
9
+ if (codePoint === undefined) {
10
+ return char;
11
+ }
12
+ return String.fromCodePoint(codePoint + 32);
13
+ });
14
+ /**
15
+ * TypeScript mirror of the SQL `arabic_normalize()` function. Folds Arabic
16
+ * orthographic variants so a query matches regardless of how the text was
17
+ * typed (alef/hamza/teh-marbuta/yeh variants, tashkeel, tatweel,
18
+ * Arabic-Indic digits), then applies locale-stable ASCII case folding for
19
+ * mixed-script names. It must NOT strip Latin diacritics — the stored match
20
+ * keys don't; client-side diacritic-insensitive matching is
21
+ * `foldSearchMatchText` (search-match.ts).
22
+ *
23
+ * NFKC runs first so presentation forms (U+FB50–FDFF, U+FE70–FEFF) fold
24
+ * to their canonical letters before the explicit folds apply.
25
+ *
26
+ * This MUST stay consistent with the SQL `arabic_normalize()` function;
27
+ * the golden vectors in normalize.test.ts pin the contract for both.
28
+ */
29
+ export const arabicNormalize = (text) => foldSearchCase(applyArabicFolds(text.normalize("NFKC")))
30
+ .replace(SEARCH_WHITESPACE_RE, " ")
31
+ .trim();
32
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,kBAAkB,GAAG,UAAU,CAAC;AACtC,MAAM,oBAAoB,GACxB,+EAA+E,CAAC;AAElF,MAAM,cAAc,GAAG,CAAC,IAAY,EAAU,EAAE,CAC9C,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE;IACxC,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;QACjB,OAAO,GAAG,CAAC;IACb,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IACtC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,aAAa,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEL;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAU,EAAE,CACtD,cAAc,CAAC,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;KACrD,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC;KAClC,IAAI,EAAE,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Diacritic-insensitive substring matching with original-index mapping.
3
+ *
4
+ * `foldSearchMatchText` builds the client-side match key: NFKD
5
+ * decomposition, the `unaccent()`-parity ASCII folds (letters with no
6
+ * decomposition: `ł`, `ø`, `đ`, `ß`), combining-mark strip, Arabic
7
+ * orthographic folds, then lowercase — so a `capek` query matches `Čapek`
8
+ * and `wroclaw` matches `Wrocław`, both ways. It is deliberately distinct
9
+ * from `arabicNormalize` (normalize.ts), which is pinned to the SQL
10
+ * `arabic_normalize()` contract and must not strip Latin diacritics.
11
+ *
12
+ * `findSearchMatchRanges` locates a folded query inside folded content and
13
+ * reports matches as ranges into the ORIGINAL string, so highlights wrap the
14
+ * text the user actually sees. Folding changes string length (per-character
15
+ * decomposition, mark removal, contextual lowercasing), so the folded form
16
+ * carries a per-code-unit map back to the originating character's range.
17
+ */
18
+ export type SearchMatchRange = {
19
+ end: number;
20
+ start: number;
21
+ };
22
+ export type FoldedSearchText = {
23
+ /** Original-string character range for each folded UTF-16 code unit. */
24
+ originalRanges: SearchMatchRange[];
25
+ text: string;
26
+ };
27
+ export declare const foldSearchMatchText: (value: string) => string;
28
+ export declare const foldSearchMatchTextWithOffsets: (content: string) => FoldedSearchText;
29
+ type FindSearchMatchRangesOptions = {
30
+ maxMatches?: number;
31
+ };
32
+ /**
33
+ * Non-overlapping occurrences of the folded query in the content, as ranges
34
+ * into the original string. Accepts pre-folded content so callers matching
35
+ * many queries against the same text fold it once.
36
+ */
37
+ export declare const findSearchMatchRanges: (content: string | FoldedSearchText, query: string, options?: FindSearchMatchRangesOptions) => SearchMatchRange[];
38
+ export {};
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Diacritic-insensitive substring matching with original-index mapping.
3
+ *
4
+ * `foldSearchMatchText` builds the client-side match key: NFKD
5
+ * decomposition, the `unaccent()`-parity ASCII folds (letters with no
6
+ * decomposition: `ł`, `ø`, `đ`, `ß`), combining-mark strip, Arabic
7
+ * orthographic folds, then lowercase — so a `capek` query matches `Čapek`
8
+ * and `wroclaw` matches `Wrocław`, both ways. It is deliberately distinct
9
+ * from `arabicNormalize` (normalize.ts), which is pinned to the SQL
10
+ * `arabic_normalize()` contract and must not strip Latin diacritics.
11
+ *
12
+ * `findSearchMatchRanges` locates a folded query inside folded content and
13
+ * reports matches as ranges into the ORIGINAL string, so highlights wrap the
14
+ * text the user actually sees. Folding changes string length (per-character
15
+ * decomposition, mark removal, contextual lowercasing), so the folded form
16
+ * carries a per-code-unit map back to the originating character's range.
17
+ */
18
+ import { applyArabicFolds } from "./arabic.js";
19
+ import { applyAsciiFolds } from "./ascii-fold.js";
20
+ const COMBINING_MARKS = /\p{M}+/gu;
21
+ // `toLowerCase` keeps the positional final sigma that Unicode case folding
22
+ // maps to σ; a lone `σ` query would otherwise miss the end of `ΟΣ`. Same
23
+ // length, so offsets are unaffected; a full `toUpperCase().toLowerCase()`
24
+ // fold is not used because it changes lengths (ß → SS) and the ASCII table
25
+ // already covers those letters.
26
+ const FINAL_SIGMA = /ς/gu;
27
+ const foldSearchMatchTextBeforeCase = (value) => applyArabicFolds(applyAsciiFolds(value.normalize("NFKD")).replace(COMBINING_MARKS, ""));
28
+ const caseFold = (value) => value.toLowerCase().replace(FINAL_SIGMA, "σ");
29
+ export const foldSearchMatchText = (value) => caseFold(foldSearchMatchTextBeforeCase(value));
30
+ export const foldSearchMatchTextWithOffsets = (content) => {
31
+ const units = [];
32
+ let originalOffset = 0;
33
+ for (const character of content) {
34
+ const start = originalOffset;
35
+ originalOffset += character.length;
36
+ units.push({
37
+ range: { start, end: originalOffset },
38
+ text: foldSearchMatchTextBeforeCase(character),
39
+ });
40
+ }
41
+ const beforeCase = units.map(({ text }) => text).join("");
42
+ const contextualLowercase = caseFold(beforeCase);
43
+ if (contextualLowercase.length === beforeCase.length) {
44
+ const originalRanges = [];
45
+ for (const unit of units) {
46
+ let codeUnit = 0;
47
+ while (codeUnit < unit.text.length) {
48
+ originalRanges.push(unit.range);
49
+ codeUnit += 1;
50
+ }
51
+ }
52
+ return {
53
+ text: contextualLowercase,
54
+ originalRanges,
55
+ };
56
+ }
57
+ // Lowercasing changed the length (e.g. İ gains a combining dot), so the
58
+ // whole-string result cannot be aligned with the per-character units;
59
+ // lowercase each unit separately instead.
60
+ const loweredParts = [];
61
+ const originalRanges = [];
62
+ for (const unit of units) {
63
+ const lowered = caseFold(unit.text);
64
+ loweredParts.push(lowered);
65
+ let codeUnit = 0;
66
+ while (codeUnit < lowered.length) {
67
+ originalRanges.push(unit.range);
68
+ codeUnit += 1;
69
+ }
70
+ }
71
+ return { text: loweredParts.join(""), originalRanges };
72
+ };
73
+ /**
74
+ * Non-overlapping occurrences of the folded query in the content, as ranges
75
+ * into the original string. Accepts pre-folded content so callers matching
76
+ * many queries against the same text fold it once.
77
+ */
78
+ export const findSearchMatchRanges = (content, query, options = {}) => {
79
+ const maxMatches = Math.max(0, Math.floor(options.maxMatches ?? Number.MAX_SAFE_INTEGER));
80
+ if (maxMatches === 0) {
81
+ return [];
82
+ }
83
+ const foldedContent = typeof content === "string"
84
+ ? foldSearchMatchTextWithOffsets(content)
85
+ : content;
86
+ const foldedQuery = foldSearchMatchText(query.trim());
87
+ if (foldedQuery.length === 0) {
88
+ return [];
89
+ }
90
+ const matches = [];
91
+ let searchFrom = 0;
92
+ while (searchFrom <= foldedContent.text.length - foldedQuery.length) {
93
+ const foldedStart = foldedContent.text.indexOf(foldedQuery, searchFrom);
94
+ if (foldedStart === -1) {
95
+ break;
96
+ }
97
+ const foldedEnd = foldedStart + foldedQuery.length;
98
+ const firstRange = foldedContent.originalRanges.at(foldedStart);
99
+ const lastRange = foldedContent.originalRanges.at(foldedEnd - 1);
100
+ if (!firstRange || !lastRange) {
101
+ break;
102
+ }
103
+ matches.push({ start: firstRange.start, end: lastRange.end });
104
+ if (matches.length >= maxMatches) {
105
+ break;
106
+ }
107
+ // An expanding fold (ß → ss) maps several folded units to one original
108
+ // character; resume after the last unit inside the matched characters so
109
+ // that character is reported once and the budget reaches later hits.
110
+ searchFrom = foldedEnd;
111
+ while ((foldedContent.originalRanges.at(searchFrom)?.start ?? Infinity) <
112
+ lastRange.end) {
113
+ searchFrom += 1;
114
+ }
115
+ }
116
+ return matches;
117
+ };
118
+ //# sourceMappingURL=search-match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-match.js","sourceRoot":"","sources":["../src/search-match.ts"],"names":[],"mappings":"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"}
package/dist/slug.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * One slug generator for the key-shaped strings the apps derive from a label.
3
+ *
4
+ * Four copies of this loop existed, differing only in their separator, their
5
+ * budget, their fallback, and whether a non-ASCII letter survives. Those four
6
+ * are the parameters; everything else — lowercase, collapse a run of rejected
7
+ * characters into one separator, clip, trim a trailing separator, fall back
8
+ * when nothing is left — was identical in all four and is fixed here.
9
+ *
10
+ * Deliberately does NOT transliterate. `slugify("café")` is `"caf"` under the
11
+ * `ascii` charset, not `"cafe"`: slugs are persisted, and folding here would
12
+ * quietly start minting different keys for the same labels. The folding
13
+ * primitives in this package are for search text, where nothing is stored.
14
+ *
15
+ * Single-pass by construction: a regex pipeline over untrusted label text is
16
+ * what the slow-regex guard exists to prevent.
17
+ */
18
+ /**
19
+ * Which characters survive.
20
+ *
21
+ * - `ascii` keeps `[a-z0-9]`, for keys that end up in URLs and columns
22
+ * validated against that charset.
23
+ * - `unicode` keeps any Unicode letter or number, for paths shown back to the
24
+ * person who typed them.
25
+ */
26
+ export type SlugCharset = "ascii" | "unicode";
27
+ /**
28
+ * The delimiters callers use: a hyphen for URL and column keys, an underscore
29
+ * for field paths.
30
+ *
31
+ * A closed union rather than a `string`, because an empty separator would make
32
+ * the trim loop below spin forever — `"".endsWith("")` is true and
33
+ * `slice(0, -0)` is the whole string. A caller cannot reach that state now.
34
+ */
35
+ export type SlugSeparator = "-" | "_";
36
+ export type SlugifyOptions = {
37
+ readonly charset: SlugCharset;
38
+ /** Joins the surviving runs. */
39
+ readonly separator: SlugSeparator;
40
+ /** Budget the result is clipped to, before the trailing separator is cut. */
41
+ readonly maxLength: number;
42
+ /** Returned when nothing survives, so a caller always gets a usable key. */
43
+ readonly fallback: string;
44
+ };
45
+ export declare const slugify: (value: string, { charset, separator, maxLength, fallback }: SlugifyOptions) => string;
package/dist/slug.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One slug generator for the key-shaped strings the apps derive from a label.
3
+ *
4
+ * Four copies of this loop existed, differing only in their separator, their
5
+ * budget, their fallback, and whether a non-ASCII letter survives. Those four
6
+ * are the parameters; everything else — lowercase, collapse a run of rejected
7
+ * characters into one separator, clip, trim a trailing separator, fall back
8
+ * when nothing is left — was identical in all four and is fixed here.
9
+ *
10
+ * Deliberately does NOT transliterate. `slugify("café")` is `"caf"` under the
11
+ * `ascii` charset, not `"cafe"`: slugs are persisted, and folding here would
12
+ * quietly start minting different keys for the same labels. The folding
13
+ * primitives in this package are for search text, where nothing is stored.
14
+ *
15
+ * Single-pass by construction: a regex pipeline over untrusted label text is
16
+ * what the slow-regex guard exists to prevent.
17
+ */
18
+ const UNICODE_SLUG_CHARACTER = /[\p{L}\p{N}]/u;
19
+ const isSlugCharacter = (character, charset) => {
20
+ if (charset === "unicode") {
21
+ return UNICODE_SLUG_CHARACTER.test(character);
22
+ }
23
+ return ((character >= "a" && character <= "z") ||
24
+ (character >= "0" && character <= "9"));
25
+ };
26
+ export const slugify = (value, { charset, separator, maxLength, fallback }) => {
27
+ let buffer = "";
28
+ let lastWasSeparator = true;
29
+ for (const character of value.toLowerCase()) {
30
+ if (isSlugCharacter(character, charset)) {
31
+ buffer += character;
32
+ lastWasSeparator = false;
33
+ continue;
34
+ }
35
+ if (!lastWasSeparator) {
36
+ buffer += separator;
37
+ lastWasSeparator = true;
38
+ }
39
+ }
40
+ let clipped = "";
41
+ for (const character of buffer) {
42
+ if (clipped.length + character.length > maxLength) {
43
+ break;
44
+ }
45
+ clipped += character;
46
+ }
47
+ // Every separator is one character, so the trim shortens by one per pass.
48
+ while (clipped.endsWith(separator)) {
49
+ clipped = clipped.slice(0, -1);
50
+ }
51
+ return clipped || fallback;
52
+ };
53
+ //# sourceMappingURL=slug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAgCH,MAAM,sBAAsB,GAAG,eAAe,CAAC;AAE/C,MAAM,eAAe,GAAG,CAAC,SAAiB,EAAE,OAAoB,EAAW,EAAE;IAC3E,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,sBAAsB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,CACL,CAAC,SAAS,IAAI,GAAG,IAAI,SAAS,IAAI,GAAG,CAAC;QACtC,CAAC,SAAS,IAAI,GAAG,IAAI,SAAS,IAAI,GAAG,CAAC,CACvC,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,CACrB,KAAa,EACb,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAkB,EACnD,EAAE;IACV,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,gBAAgB,GAAG,IAAI,CAAC;IAE5B,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;QAC5C,IAAI,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,SAAS,CAAC;YACpB,gBAAgB,GAAG,KAAK,CAAC;YACzB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACtB,MAAM,IAAI,SAAS,CAAC;YACpB,gBAAgB,GAAG,IAAI,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,SAAS,IAAI,MAAM,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;YAClD,MAAM;QACR,CAAC;QACD,OAAO,IAAI,SAAS,CAAC;IACvB,CAAC;IACD,0EAA0E;IAC1E,OAAO,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,OAAO,IAAI,QAAQ,CAAC;AAC7B,CAAC,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Collapse spaced-out letters used for emphasis in court PDFs.
3
+ *
4
+ * Slovak and Czech courts format words with letter-spacing:
5
+ * `r o z h o d o l :` -> `rozhodol:`
6
+ * `o d ô v o d n e n i e :` -> `odôvodnenie:`
7
+ * `z a m i e t a` -> `zamieta`
8
+ *
9
+ * These break full-text search ("rozhodol" won't match
10
+ * "r o z h o d o l"). We collapse runs of single Unicode letters
11
+ * separated by single spaces, optionally followed by punctuation.
12
+ *
13
+ * Requires at least FOUR letters in the run. Czech/Slovak have many
14
+ * single-letter words (prepositions a, i, k, o, s, u, v, z), so a 2–3
15
+ * letter run like `u a v` ("u", "a", "v") is far more likely to be real
16
+ * words than letter-spaced emphasis; collapsing it to `uav` would corrupt
17
+ * the text. Genuine spaced words ("z a m i e t a", "r o z h o d o l") are
18
+ * always longer, so the floor loses nothing in practice.
19
+ *
20
+ * This is the single source of the threshold: the ingestion pipeline
21
+ * (index-time) and the case-viewer find-in-page normalizer (query-time)
22
+ * both consume it, so a spaced heading collapses identically on both
23
+ * sides and highlight offsets stay aligned.
24
+ */
25
+ /**
26
+ * A fresh global RegExp matching one spaced-letter run. Returned from a
27
+ * factory (not a shared constant) because callers use it with both
28
+ * `String.replace` and `String.matchAll`; a fresh instance avoids any
29
+ * shared `lastIndex` surprises.
30
+ */
31
+ export declare const spacedLetterRunRegex: () => RegExp;
32
+ /**
33
+ * Collapse every spaced-letter run in `text` to its concatenated letters,
34
+ * normalizing multi-spaces to a single space.
35
+ *
36
+ * Applied to a fixpoint rather than once, so `f(f(x)) === f(x)` holds for
37
+ * every input. A single pass did not settle: the pattern matches letters
38
+ * separated by exactly one space, while the multi-space squeeze runs
39
+ * afterwards, so a run written with two spaces survived the pass and came
40
+ * out in the very shape the pattern was looking for. The next call then
41
+ * collapsed what the previous one had not.
42
+ *
43
+ * That mattered because the two sides of a search comparison are folded
44
+ * at different times — index time during ingestion, query time in the
45
+ * case-viewer's find-in-page — and the query side is handed text the
46
+ * index side already collapsed. A function that collapsed more on its
47
+ * second application made those two sides disagree, which is exactly the
48
+ * alignment this module exists to guarantee.
49
+ *
50
+ * Widening the pattern's separator to `+` would also settle it in one
51
+ * pass, and is wrong: a single space separates the letters of one
52
+ * emphasized word, and a wider gap separates two of them. Accepting both
53
+ * as letter separators collapses `Ž a l o b a s e z a m í t á` to
54
+ * `Žalobasezamítá` — the whole verdict as one word. The loop keeps that
55
+ * distinction inside each pass.
56
+ */
57
+ export declare const collapseSpacedLetters: (text: string) => string;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Collapse spaced-out letters used for emphasis in court PDFs.
3
+ *
4
+ * Slovak and Czech courts format words with letter-spacing:
5
+ * `r o z h o d o l :` -> `rozhodol:`
6
+ * `o d ô v o d n e n i e :` -> `odôvodnenie:`
7
+ * `z a m i e t a` -> `zamieta`
8
+ *
9
+ * These break full-text search ("rozhodol" won't match
10
+ * "r o z h o d o l"). We collapse runs of single Unicode letters
11
+ * separated by single spaces, optionally followed by punctuation.
12
+ *
13
+ * Requires at least FOUR letters in the run. Czech/Slovak have many
14
+ * single-letter words (prepositions a, i, k, o, s, u, v, z), so a 2–3
15
+ * letter run like `u a v` ("u", "a", "v") is far more likely to be real
16
+ * words than letter-spaced emphasis; collapsing it to `uav` would corrupt
17
+ * the text. Genuine spaced words ("z a m i e t a", "r o z h o d o l") are
18
+ * always longer, so the floor loses nothing in practice.
19
+ *
20
+ * This is the single source of the threshold: the ingestion pipeline
21
+ * (index-time) and the case-viewer find-in-page normalizer (query-time)
22
+ * both consume it, so a spaced heading collapses identically on both
23
+ * sides and highlight offsets stay aligned.
24
+ */
25
+ // `\p{L} (?:\p{L} ){2,}\p{L}` is four or more spaced letters: one leading
26
+ // letter, at least two interior letters, one trailing letter. Anchored by
27
+ // whitespace/string boundaries so it never touches normal words, digits,
28
+ // or case references.
29
+ const buildSpacedLetterRunSource = () => "(?<=\\s|^)(?:\\p{L} (?:\\p{L} ){2,}\\p{L})(?: ?[,:;.!?])?(?=\\s|$)";
30
+ /**
31
+ * A fresh global RegExp matching one spaced-letter run. Returned from a
32
+ * factory (not a shared constant) because callers use it with both
33
+ * `String.replace` and `String.matchAll`; a fresh instance avoids any
34
+ * shared `lastIndex` surprises.
35
+ */
36
+ export const spacedLetterRunRegex = () => new RegExp(buildSpacedLetterRunSource(), "gu");
37
+ const MULTI_SPACE_RE = / {2,}/gu;
38
+ const SPACE_RE = / /gu;
39
+ // Single-entry cache: sk-courts.ts's classifiers (isHoldingMarker,
40
+ // isReasoningMarker, isInstructionMarker) each re-normalize the same line
41
+ // text in sequence while classifying a heading, so consecutive calls on an
42
+ // identical string are common during ingestion. Deterministic pure
43
+ // function, so caching only the most recent call is always correct — a
44
+ // cache miss just falls through to the normal computation.
45
+ let lastInput;
46
+ let lastResult;
47
+ /**
48
+ * One collapse pass: join every spaced-letter run, then normalize any
49
+ * resulting multi-spaces to a single space.
50
+ *
51
+ * A pass only ever deletes space characters, so it either leaves the
52
+ * text alone or makes it strictly shorter. That is what bounds the loop
53
+ * below.
54
+ */
55
+ const collapsePass = (text) => text
56
+ .replace(spacedLetterRunRegex(), (match) => match.replace(SPACE_RE, ""))
57
+ .replace(MULTI_SPACE_RE, " ");
58
+ /**
59
+ * Collapse every spaced-letter run in `text` to its concatenated letters,
60
+ * normalizing multi-spaces to a single space.
61
+ *
62
+ * Applied to a fixpoint rather than once, so `f(f(x)) === f(x)` holds for
63
+ * every input. A single pass did not settle: the pattern matches letters
64
+ * separated by exactly one space, while the multi-space squeeze runs
65
+ * afterwards, so a run written with two spaces survived the pass and came
66
+ * out in the very shape the pattern was looking for. The next call then
67
+ * collapsed what the previous one had not.
68
+ *
69
+ * That mattered because the two sides of a search comparison are folded
70
+ * at different times — index time during ingestion, query time in the
71
+ * case-viewer's find-in-page — and the query side is handed text the
72
+ * index side already collapsed. A function that collapsed more on its
73
+ * second application made those two sides disagree, which is exactly the
74
+ * alignment this module exists to guarantee.
75
+ *
76
+ * Widening the pattern's separator to `+` would also settle it in one
77
+ * pass, and is wrong: a single space separates the letters of one
78
+ * emphasized word, and a wider gap separates two of them. Accepting both
79
+ * as letter separators collapses `Ž a l o b a s e z a m í t á` to
80
+ * `Žalobasezamítá` — the whole verdict as one word. The loop keeps that
81
+ * distinction inside each pass.
82
+ */
83
+ export const collapseSpacedLetters = (text) => {
84
+ if (text === lastInput && lastResult !== undefined) {
85
+ return lastResult;
86
+ }
87
+ let result = text;
88
+ for (;;) {
89
+ const next = collapsePass(result);
90
+ // Terminates: a pass that changes anything strictly shortens the
91
+ // text, and a pass that changes nothing ends the loop.
92
+ if (next === result) {
93
+ break;
94
+ }
95
+ result = next;
96
+ }
97
+ lastInput = text;
98
+ lastResult = result;
99
+ return result;
100
+ };
101
+ //# sourceMappingURL=spaced-letters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spaced-letters.js","sourceRoot":"","sources":["../src/spaced-letters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,0EAA0E;AAC1E,0EAA0E;AAC1E,yEAAyE;AACzE,sBAAsB;AACtB,MAAM,0BAA0B,GAAG,GAAG,EAAE,CACtC,oEAAoE,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAW,EAAE,CAC/C,IAAI,MAAM,CAAC,0BAA0B,EAAE,EAAE,IAAI,CAAC,CAAC;AAEjD,MAAM,cAAc,GAAG,SAAS,CAAC;AACjC,MAAM,QAAQ,GAAG,KAAK,CAAC;AAEvB,mEAAmE;AACnE,0EAA0E;AAC1E,2EAA2E;AAC3E,mEAAmE;AACnE,uEAAuE;AACvE,2DAA2D;AAC3D,IAAI,SAA6B,CAAC;AAClC,IAAI,UAA8B,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE,CAC5C,IAAI;KACD,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;KACvE,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,IAAY,EAAU,EAAE;IAC5D,IAAI,IAAI,KAAK,SAAS,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QACnD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,SAAS,CAAC;QACR,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAClC,iEAAiE;QACjE,uDAAuD;QACvD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,MAAM;QACR,CAAC;QACD,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IAED,SAAS,GAAG,IAAI,CAAC;IACjB,UAAU,GAAG,MAAM,CAAC;IACpB,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,9 +1,58 @@
1
1
  {
2
2
  "name": "@stll/text-normalize",
3
- "version": "0.0.1-placeholder.0",
4
- "description": "Placeholder to bootstrap npm trusted publishing. Not for use.",
5
- "license": "Apache-2.0",
3
+ "version": "0.1.1",
4
+ "description": "Search match-key text normalization — Arabic orthographic variant folding (alef/hamza/teh-marbuta/yeh, tashkeel, tatweel, digits) with per-language fold tables",
5
+ "keywords": [
6
+ "arabic",
7
+ "i18n",
8
+ "normalization",
9
+ "search",
10
+ "text-search",
11
+ "unicode"
12
+ ],
6
13
  "homepage": "https://github.com/stella/stella/tree/main/packages/text-normalize",
7
- "bugs": { "url": "https://github.com/stella/stella/issues" },
8
- "repository": { "type": "git", "url": "git+https://github.com/stella/stella.git", "directory": "packages/text-normalize" }
14
+ "bugs": {
15
+ "url": "https://github.com/stella/stella/issues"
16
+ },
17
+ "license": "Apache-2.0",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/stella/stella.git",
21
+ "directory": "packages/text-normalize"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "clean": "git clean -xdf dist .cache .turbo node_modules",
42
+ "build": "rm -rf dist && bun ../../packages/scripts/src/tsc-native.ts -p tsconfig.build.json",
43
+ "pack:dry-run": "bun pm pack --dry-run",
44
+ "test": "bun test src",
45
+ "test:property": "bun test --preload @stll/property-testing/preload $(grep -rlF 'fc.assert' src --include='*.test.ts')",
46
+ "typecheck": "bun ../../packages/scripts/src/tsc-native.ts --noEmit",
47
+ "lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --type-aware --type-check packages/text-normalize",
48
+ "lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/text-normalize",
49
+ "format": "bun ../../scripts/run-oxfmt.ts .",
50
+ "prepack": "bun run build"
51
+ },
52
+ "devDependencies": {
53
+ "@stll/property-testing": "0.0.0",
54
+ "@stll/typescript-config": "0.0.0",
55
+ "bun-types": "1.4.2",
56
+ "fast-check": "^4.10.1"
57
+ }
9
58
  }