Skip to content

@absoluteui/helpers/string

Case conversion, grapheme-safe truncation, slugs, masking, templates, fuzzy search, and escaping.

import { ... } from '@absoluteui/helpers/string';

Unicode-aware text helpers for what applications display and search: fuzzy search with highlighting, safe templates, plurals and lists, masking, slugs, and case conversion. Length and truncation count user-perceived characters (grapheme clusters), so emoji and accented names are never cut in half. Formatting that depends on language takes a locale; for dates and numbers, see the Internationalization guide.

How do I search a people list and highlight the match?

Section titled “How do I search a people list and highlight the match?”

fuzzyMatch ranks candidates across several keys (accents ignored by default); highlight splits the text into matched and unmatched segments, so you render the match with DOM nodes instead of HTML strings.

import { fuzzyMatch, highlight } from '@absoluteui/helpers/string';
const people = [
{ name: 'Zoë Martin', email: 'zoe.martin@example.com' },
{ name: 'Marta Olsen', email: 'marta.o@example.com' },
{ name: 'Ben Ross', email: 'ben.ross@example.com' },
];
const hits = fuzzyMatch('zoe', people, { keys: ['name', 'email'], limit: 5 });
hits.map((h) => h.item.name); // => ['Zoë Martin']
highlight('Zoë Martin', 'zoe'); // => [{ text: 'Zoë', match: true }, { text: ' Martin', match: false }]

How do I build a notification message safely?

Section titled “How do I build a notification message safely?”

template fills {placeholders} without evaluating code; pass escape: escapeHtml when the result goes into HTML. pluralize picks the plural form for the locale and formatList joins names.

import { escapeHtml, formatList, pluralize, template } from '@absoluteui/helpers/string';
const approvers = ['Maya', 'Omar', 'Lin'];
const days = pluralize(3, { one: '{count} day', other: '{count} days' }, { locale: 'en' });
const text = template('{who} requested {days} of leave. Approvers: {list}.', {
who: 'Ben <b>',
days,
list: formatList(approvers, { locale: 'en' }),
}, { escape: escapeHtml });
text; // => 'Ben &lt;b&gt; requested 3 days of leave. Approvers: Maya, Omar, and Lin.'

How do I hide personal data in lists and logs?

Section titled “How do I hide personal data in lists and logs?”

mask keeps a chosen number of characters visible and can skip formatting characters; maskEmail keeps the first character and the domain.

import { mask, maskEmail } from '@absoluteui/helpers/string';
mask('DE89 3704 0044 0532 0130 00', { visibleEnd: 4, preserve: ' ' }); // => '•••• •••• •••• •••• ••30 00'
mask('123-45-6789', { visibleEnd: 4, preserve: '-', maskChar: '*' }); // => '***-**-6789'
maskEmail('asha.rao@example.com'); // => 'a•••••••@example.com'

How do I make file names, URLs, and avatar labels from names?

Section titled “How do I make file names, URLs, and avatar labels from names?”

slugify removes accents and punctuation (Unicode letters are kept unless ascii: true); initials and truncate fit names into small spaces.

import { initials, slugify, truncate } from '@absoluteui/helpers/string';
`${slugify('Zoë Martin – Q3 Review', { maxLength: 40 })}.pdf`; // => 'zoe-martin-q3-review.pdf'
initials('Zoë Martin'); // => 'ZM'
truncate('Senior Payroll and Benefits Specialist', 20, { wordBoundary: true }); // => 'Senior Payroll and…'

How do I convert between naming conventions?

Section titled “How do I convert between naming conventions?”

Case helpers split words on separators, case changes, and digits, so API field names map cleanly to column ids and labels.

import { camelCase, constantCase, sentenceCase, snakeCase } from '@absoluteui/helpers/string';
camelCase('hire_date'); // => 'hireDate'
snakeCase('employeeId'); // => 'employee_id'
sentenceCase('annualLeaveBalance'); // => 'Annual leave balance'
constantCase('leave-type'); // => 'LEAVE_TYPE'

between

Text between the first start and the next end; undefined when either is missing.

function between(s: string, start: string, end: string): string | undefined;

Example:

between('key=[value]', '[', ']'); // => 'value'

byteLengthUtf8

UTF-8 byte length without allocating (lone surrogates count as U+FFFD, 3 bytes).

function byteLengthUtf8(s: string): number;

Example:

byteLengthUtf8('€'); // => 3

camelCase

camelCase. Acronyms are lowered (XMLHttpRequest -> xmlHttpRequest).

function camelCase(s: string, options?: LocaleOptions): string;

Example:

camelCase('user_first-name'); // => 'userFirstName'

capitalize

Uppercases the first character (locale-aware: { locale: 'tr' } maps i -> İ).

function capitalize(s: string, options?: CapitalizeOptions): string;

Example:

capitalize('élan'); // => 'Élan'

center

Centers s in width grapheme columns with fill.

function center(s: string, width: number, fill?: string): string;

Example:

center('ab', 6, '*'); // => '**ab**'

charCount

Length in the given unit: grapheme (default), codePoint, or codeUnit (UTF-16).

function charCount(s: string, options?: {
readonly unit?: 'grapheme' | 'codePoint' | 'codeUnit';
}): number;

Example:

charCount('😀', { unit: 'codeUnit' }); // => 2

collapseWhitespace

Trims and collapses every whitespace run (including newlines) to one space.

function collapseWhitespace(s: string): string;

Example:

collapseWhitespace(' a \n\t b '); // => 'a b'

compareIgnoreCase

Case-insensitive locale comparison (-1, 0, 1) for sorting.

function compareIgnoreCase(a: string, b: string, options?: IgnoreCaseOptions): -1 | 0 | 1;

Example:

['b', 'A', 'a'].sort((x, y) => compareIgnoreCase(x, y)); // => ['A', 'a', 'b']

constantCase

CONSTANT_CASE.

function constantCase(s: string, options?: LocaleOptions): string;

Example:

constantCase('maxRetryCount'); // => 'MAX_RETRY_COUNT'

containsIgnoreCase

Case-insensitive (optionally accent-insensitive) substring test with locale-aware case folding.

function containsIgnoreCase(haystack: string, needle: string, options?: IgnoreCaseOptions): boolean;

Example:

containsIgnoreCase('Crème Brûlée', 'BRULEE', { ignoreAccents: true }); // => true

countOccurrences

Counts occurrences of a literal substring (non-overlapping by default).

function countOccurrences(s: string, sub: string, options?: {
readonly overlapping?: boolean;
}): number;

Example:

countOccurrences('aaaa', 'aa'); // => 2
countOccurrences('aaaa', 'aa', { overlapping: true }); // => 3

damerauLevenshtein

Damerau-Levenshtein (optimal string alignment): adjacent transpositions cost 1. O(min(n, m)) memory.

function damerauLevenshtein(a: string, b: string, options?: DistanceOptions): number;

Example:

damerauLevenshtein('ca', 'ac'); // => 1

dedent

Removes the longest common leading whitespace of non-blank lines.

function dedent(s: string): string;

Example:

dedent(' a\n b'); // => 'a\n b'

dotCase

dot.case.

function dotCase(s: string, options?: LocaleOptions): string;

Example:

dotCase('userFirstName'); // => 'user.first.name'

endsWithAny

True when s ends with any suffix.

function endsWithAny(s: string, suffixes: Iterable<string>): boolean;

Example:

endsWithAny('photo.JPG'.toLowerCase(), ['.jpg', '.png']); // => true

equalsIgnoreCase

Case-insensitive equality via Intl.Collator (sensitivity accent, or base with ignoreAccents).

function equalsIgnoreCase(a: string, b: string, options?: IgnoreCaseOptions): boolean;

Example:

equalsIgnoreCase('Straße', 'STRASSE', { locale: 'de' }); // true where the collator treats ß as ss

escapeHtml

Escapes & &lt; > " ' for HTML text and attribute values. Not a sanitizer.

function escapeHtml(s: string): string;

Example:

escapeHtml('<a href="x">Tom & Jerry\'s</a>'); // => '&lt;a href=&quot;x&quot;&gt;Tom &amp; Jerry&#39;s&lt;/a&gt;'

escapeRegExp

Escapes RegExp syntax characters so s matches literally (valid with and without the u flag).

function escapeRegExp(s: string): string;

Example:

new RegExp(escapeRegExp('1+1=2?')).test('1+1=2?'); // => true

formatList

Locale-aware list with Intl.ListFormat.

function formatList(items: Iterable<string>, options?: FormatListOptions): string;

Example:

formatList(['red', 'green', 'blue']); // => 'red, green, and blue' // en
formatList(['tea', 'coffee'], { type: 'disjunction' }); // => 'tea or coffee'

fuzzyMatch

Ranked fuzzy search: case- and accent-insensitive substring and subsequence matching with word-start and contiguity bonuses. Results carry ranges for accessible highlighting. Ties keep shorter texts first, then input order. An empty query returns every candidate with score 1.

function fuzzyMatch<T>(query: string, candidates: readonly T[], options?: FuzzyOptions<T>): FuzzyMatch<T>[];

Example:

const [best] = fuzzyMatch('jsmi', ['John Smith', 'Jane Doe']);
best?.item; // => 'John Smith'
best?.ranges; // => [[0, 1], [5, 8]]
const people = [{ name: 'Joanna Kay', email: 'jkay@x.io' }, { name: 'Ben Ross', email: 'ann.ross@x.io' }, { name: 'Cy Diaz', email: 'cy@x.io' }];
fuzzyMatch('ann', people, { keys: ['name', (p) => p.email], limit: 5 }).map((m) => m.item.name); // => ['Ben Ross', 'Joanna Kay']

graphemeLength

Number of user-perceived characters.

function graphemeLength(s: string): number;

Example:

graphemeLength('👨\u200d👩\u200d👧'); // => 1

graphemes

User-perceived characters (extended grapheme clusters via Intl.Segmenter, with a fallback).

function graphemes(s: string): string[];

Example:

graphemes('👍🏽e\u0301'); // => ['👍🏽', 'e\u0301'] // 2 graphemes

highlight

Splits text into matched and unmatched segments for a query (case/accent-insensitive) or for explicit ranges (from fuzzyMatch). Render matches with &lt;mark> using textContent.

function highlight(text: string, query: string | readonly TextRange[], options?: HighlightOptions): HighlightSegment[];

Example:

highlight('Crème brûlée', 'brulee'); // => [{ text: 'Crème ', match: false }, { text: 'brûlée', match: true }]

indent

Prefixes each line with indent (a string or a number of spaces). Blank lines stay blank unless indentEmpty.

function indent(s: string, prefix?: string | number, options?: {
readonly indentEmpty?: boolean;
}): string;

Example:

indent('a\nb', 2); // => ' a\n b'

initials

Initials from a name (grapheme-safe, locale-aware uppercase). With more words than max, keeps the first max - 1 and the last.

function initials(name: string, options?: LocaleOptions & {
readonly max?: number;
}): string;

Example:

initials('John Ronald Reuel Tolkien'); // => 'JT'
initials('élodie durand'); // => 'ÉD'

isAlpha

True for one or more Unicode letters (combining marks allowed after a letter).

function isAlpha(s: string): boolean;

Example:

isAlpha('Ñandú'); // => true

isAlphanumeric

True for one or more Unicode letters or decimal digits.

function isAlphanumeric(s: string): boolean;

Example:

isAlphanumeric('abc123'); // => true

isNumeric

True for one or more Unicode decimal digits (\p{Nd}, including non-Latin digits such as ٣).

function isNumeric(s: string): boolean;

Example:

isNumeric('0123'); // => true
isNumeric('1.5'); // => false

kebabCase

kebab-case.

function kebabCase(s: string, options?: LocaleOptions): string;

Example:

kebabCase('XMLHttpRequest'); // => 'xml-http-request'

levenshtein

Levenshtein distance over code points: O(n|m) time, O(min(n, m)) memory, optional early exit.

function levenshtein(a: string, b: string, options?: DistanceOptions): number;

Example:

levenshtein('kitten', 'sitting'); // => 3
levenshtein('kitten', 'sitting', { maxDistance: 1 }); // => 2 // exceeded

mask

Masks all but the visible start/end characters (grapheme-safe). If the visible parts would reveal everything, everything is masked.

function mask(s: string, options?: MaskOptions): string;

Example:

mask('4111111111111111', { visibleEnd: 4 }); // => '••••••••••••1111'
mask('4111 1111 1111 1111', { visibleEnd: 4, preserve: ' ' }); // => '•••• •••• •••• 1111'

maskEmail

Masks the local part of an email address.

function maskEmail(email: string, options?: MaskEmailOptions): string;

Example:

maskEmail('john.doe@example.com'); // => 'j•••••••@example.com'
maskEmail('ann@example.com', { maskDomain: true }); // => 'a••@e••••••.com'

normalizeWhitespace

Normalizes line endings (CRLF, CR, U+2028/9 -> LF), turns other whitespace (tabs, NBSP, thin spaces) into plain spaces, and drops BOM/zero-width spaces. Structure (runs, newlines) is kept.

function normalizeWhitespace(s: string): string;

Example:

normalizeWhitespace('a\u00a0b\r\nc'); // => 'a b\nc'

pascalCase

PascalCase.

function pascalCase(s: string, options?: LocaleOptions): string;

Example:

pascalCase('user first name'); // => 'UserFirstName'

pluralize

Picks the plural form for count with Intl.PluralRules and replaces {count} with the locale-formatted number. An explicit zero form wins for 0 in every locale.

function pluralize(count: number, forms: PluralForms, options?: LocaleOptions & {
readonly type?: Intl.PluralRuleType;
}): string;

Example:

pluralize(1, { one: '{count} file', other: '{count} files' }); // => '1 file'
pluralize(3, { one: '{count} plik', few: '{count} pliki', many: '{count} plików', other: '{count} pliku' }, { locale: 'pl' }); // => '3 pliki'

removeDiacritics

Removes Latin/Greek/Cyrillic diacritics (é -> e, ł -> l); vowel signs of other scripts are kept.

function removeDiacritics(s: string): string;

Example:

removeDiacritics('Crème Brûlée, Łódź'); // => 'Creme Brulee, Lodz'

replaceAllSafe

Replaces every occurrence of a literal string; $ patterns in replacement are not interpreted.

function replaceAllSafe(s: string, search: string, replacement: string): string;

Example:

replaceAllSafe('a.b.c', '.', '$&'); // => 'a$&b$&c'

reverseGraphemes

Reverses by grapheme clusters, keeping emoji and combining marks intact.

function reverseGraphemes(s: string): string;

Example:

reverseGraphemes('ab👍🏽'); // => '👍🏽ba'

sentenceCase

Sentence case (first word capitalized, words joined by spaces).

function sentenceCase(s: string, options?: LocaleOptions): string;

Example:

sentenceCase('userFirstName'); // => 'User first name'

similarity

Normalized similarity 0-1 (1 - Levenshtein / longer length); two empty strings are 1.

function similarity(a: string, b: string, options?: IgnoreCaseOptions & {
readonly ignoreCase?: boolean;
}): number;

Example:

similarity('color', 'colour'); // => ≈ 0.833

slugify

URL slug: NFKD, diacritics removed, locale-aware lowercasing, non-word runs to one separator.

function slugify(s: string, options?: SlugifyOptions): string;

Example:

slugify('Crème Brûlée & Café!'); // => 'creme-brulee-cafe'
slugify('İstanbul', { locale: 'tr' }); // => 'istanbul'

snakeCase

snake_case.

function snakeCase(s: string, options?: LocaleOptions): string;

Example:

snakeCase('userFirstName'); // => 'user_first_name'

splitLines

Splits on LF, CRLF, CR, U+2028 and U+2029.

function splitLines(s: string): string[];

Example:

splitLines('a\r\nb\nc'); // => ['a', 'b', 'c']

startsWithAny

True when s starts with any prefix.

function startsWithAny(s: string, prefixes: Iterable<string>): boolean;

Example:

startsWithAny('https://x', ['http://', 'https://']); // => true

stripControlChars

Removes C0/C1 control characters and, by default, bidi override controls.

function stripControlChars(s: string, options?: StripControlOptions): string;

Example:

stripControlChars('a\u0000b\u202ec\n'); // => 'abc\n'

template

Safe {name} / {a.b} interpolation: no code evaluation, own properties only, unsafe keys (__proto__, constructor, prototype) never resolve. {{ and }} produce literal braces. Primitives are stringified, dates use ISO 8601; objects count as missing.

function template(str: string, values: object, options?: TemplateOptions): string;

Example:

template('Hello {user.name}, you have {count} tasks', { user: { name: 'Ann' }, count: 3 }); // => 'Hello Ann, you have 3 tasks'

titleCase

Title Case from any identifier or phrase (words joined by spaces).

function titleCase(s: string, options?: TitleCaseOptions): string;

Example:

titleCase('the_lord-of the rings', { minorWords: ENGLISH_MINOR_WORDS }); // => 'The Lord of the Rings'

toCodePoints

Code points of s.

function toCodePoints(s: string): number[];

Example:

toCodePoints('a😀'); // => [97, 128512]

truncate

Truncates to at most length grapheme clusters including the omission. Never splits emoji or accents.

function truncate(s: string, length: number, options?: TruncateOptions): string;

Example:

truncate('Hello wonderful world', 12, { wordBoundary: true }); // => 'Hello…'
truncate('👍🏽👍🏽👍🏽', 2); // => '👍🏽…'

truncateMiddle

Truncates in the middle, keeping the start and end (file names, IDs).

function truncateMiddle(s: string, length: number, options?: {
readonly omission?: string;
}): string;

Example:

truncateMiddle('annual-report-2026-final.pdf', 15); // => 'annual-…nal.pdf'

uncapitalize

Lowercases the first character.

function uncapitalize(s: string, options?: LocaleOptions): string;

Example:

uncapitalize('Hello World'); // => 'hello World'

unescapeHtml

Decodes &amp; &lt; &gt; &quot; &apos; &nbsp; and numeric references (&#39;, &#x27;). Invalid code points become U+FFFD.

function unescapeHtml(s: string): string;

Example:

unescapeHtml('Tom &amp; Jerry&#39;s &#x1F600;'); // => "Tom & Jerry's 😀"

wordCount

Word count using Intl.Segmenter word boundaries (works for scripts without spaces, such as Japanese).

function wordCount(s: string, options?: LocaleOptions): number;

Example:

wordCount("Don't stop, it's 2026!"); // => 4

wrapText

Greedy word wrap to width grapheme columns; existing line breaks are kept.

function wrapText(s: string, width: number, options?: WrapTextOptions): string;

Example:

wrapText('The quick brown fox', 10); // => 'The quick\nbrown fox'

ENGLISH_MINOR_WORDS

Common English minor words for titleCase({ minorWords }).

const ENGLISH_MINOR_WORDS: readonly string[];

Example:

titleCase('war and peace', { minorWords: ENGLISH_MINOR_WORDS }); // => 'War and Peace'

CapitalizeOptions

interface CapitalizeOptions extends LocaleOptions {
readonly lowerRest?: boolean;
}

DistanceOptions

interface DistanceOptions {
readonly maxDistance?: number;
}

FormatListOptions

interface FormatListOptions extends LocaleOptions {
readonly type?: Intl.ListFormatType;
readonly style?: Intl.ListFormatStyle;
}

FuzzyMatch

interface FuzzyMatch<T> {
readonly item: T;
readonly index: number;
readonly score: number;
readonly key: number;
readonly text: string;
readonly ranges: readonly TextRange[];
}

FuzzyOptions

interface FuzzyOptions<T> extends LocaleOptions {
readonly limit?: number;
readonly threshold?: number;
readonly keys?: readonly FuzzyKey<T>[];
readonly ignoreAccents?: boolean;
}

HighlightOptions

interface HighlightOptions extends LocaleOptions {
readonly ignoreAccents?: boolean;
readonly words?: boolean;
}

HighlightSegment

A piece of text and whether it matched, for rendering &lt;mark> via textContent (no HTML).

interface HighlightSegment {
readonly text: string;
readonly match: boolean;
}

IgnoreCaseOptions

interface IgnoreCaseOptions extends LocaleOptions {
readonly ignoreAccents?: boolean;
}

LocaleOptions

Options for locale-sensitive helpers. Without locale, case mapping is locale-independent.

interface LocaleOptions {
readonly locale?: Intl.LocalesArgument;
}

MaskEmailOptions

interface MaskEmailOptions {
readonly maskChar?: string;
readonly visible?: number;
readonly maskDomain?: boolean;
}

MaskOptions

interface MaskOptions {
readonly visibleStart?: number;
readonly visibleEnd?: number;
readonly maskChar?: string;
readonly preserve?: string;
}

PluralForms

Plural forms keyed by Intl.PluralRules category; other is required.

interface PluralForms {
readonly zero?: string;
readonly one?: string;
readonly two?: string;
readonly few?: string;
readonly many?: string;
readonly other: string;
}

SlugifyOptions

interface SlugifyOptions extends LocaleOptions {
readonly separator?: string;
readonly lowercase?: boolean;
readonly ascii?: boolean;
readonly maxLength?: number;
}

StripControlOptions

interface StripControlOptions {
readonly keepWhitespace?: boolean;
readonly stripBidi?: boolean;
}

TemplateOptions

interface TemplateOptions {
readonly missing?: 'keep' | 'empty' | 'error';
readonly escape?: (value: string) => string;
}

TitleCaseOptions

interface TitleCaseOptions extends LocaleOptions {
readonly minorWords?: Iterable<string>;
}

TruncateOptions

interface TruncateOptions {
readonly omission?: string;
readonly wordBoundary?: boolean;
}

WrapTextOptions

interface WrapTextOptions {
readonly breakWords?: boolean;
readonly newline?: string;
}

FuzzyKey

type FuzzyKey<T> = ((item: T) => string | null | undefined) | {
[K in keyof T]: T[K] extends string | null | undefined ? K : never;
}[keyof T];

TextRange

Half-open [start, end) UTF-16 range in the original text.

type TextRange = readonly [start: number, end: number];

splitWords

Splits identifiers and phrases into words: separators, lower->upper humps, acronym boundaries (XMLHttpRequest -> XML, Http, Request), optionally digits. Combining marks stay with their letter; apostrophes inside words are kept (don't).

function splitWords(input: string, options?: SplitWordsOptions): string[];

Example:

splitWords('XMLHttpRequest'); // => ['XML', 'Http', 'Request']
splitWords('öffne_DieTür-2x'); // => ['öffne', 'Die', 'Tür', '2x']

SplitWordsOptions

Unicode-aware word splitting for case conversion.

interface SplitWordsOptions {
readonly splitDigits?: boolean;
}