platform-bible-utils
    Preparing search index...

    Class GraphemeString

    A string pre-segmented into Unicode grapheme clusters. Segmentation happens once in the constructor (the expensive step); every other operation reuses it. Derived values (substring/slice/etc.) reuse the parent grapheme slice rather than re-segmenting. The single exception is a regular expression's capture groups, whose text the parent's segmentation does not cover — a group can match across a boundary the parent never had.

    Every method mirrors its String.prototype counterpart exactly — including the edge cases around negative, fractional, NaN, and out-of-range arguments — with one substitution: the unit of indexing and length is the grapheme cluster rather than the UTF-16 code unit. So length counts what a reader would call characters, slice never cuts a cluster in half, and a search only reports a hit that begins and ends on cluster boundaries.

    The surface is limited to operations that actually need the segmentation: toArray and formatReplacement have no native counterpart but do need it, while normalize and ordinalCompare are deliberately absent, because neither reads the string as characters — they live with the plain string helpers in string-util instead.

    Range methods return a GraphemeString rather than a string so the parent's segmentation carries into the result instead of being recomputed. Call toString() for the text. The padding methods return text instead, because they are the one pair that adds characters: added text can fuse with the text it lands against, so there is no segmentation to carry.

    Clusters come from unicode-segmenter, which implements UAX #29 extended grapheme clusters against Unicode 17. length counts what a literate reader of the script would call characters — for emoji and Latin, and equally for pointed Hebrew, vocalized Arabic and Syriac, Indic conjuncts, Thai, and Hangul written as decomposed Jamo.

    Two consequences deserve attention, because they are where a conformant segmenter changes an answer a caller might be relying on.

    \r\n is a single cluster (rule GB3). Since searches only report boundary-aligned hits, '\n' is therefore NOT findable inside a \r\n, and split on '\n' will not break Windows-style lines apart. Split lines with a regex that matches the whole terminator (/\r?\n/) rather than the bare line feed.

    A zero-width joiner attaches to the character before it (rule GB9), not the one after. So 'a\u200d ' is two clusters — 'a\u200d' and a space — and that trailing space is a cluster that is entirely whitespace.

    // Segment once, then run as many operations as you like against that work.
    const name = new GraphemeString('👨‍👩‍👧‍👦 Family');
    name.length; // 8 — the family emoji counts as one
    name.slice(0, 1).toString(); // '👨‍👩‍👧‍👦' — never cuts a cluster in half
    name.indexOf('Family'); // 2
    Index

    Constructors

    Accessors

    Methods

    • Iterate the grapheme clusters, so Array.from(...) and spreading behave the way they do on a native string — with clusters as the unit. Without this an instance would read as array-like, and Array.from would silently produce a run of undefined instead of failing.

      Returns IterableIterator<string>

      An iterator over the grapheme clusters, in order.

    • Mirrors String.prototype.at. The grapheme at index, or undefined if out of bounds. Negative indexes count back from the end.

      Parameters

      • index: number

        Grapheme index. Negative counts back from the end; fractional truncates toward zero and NaN becomes 0.

      Returns undefined | string

      The grapheme cluster at index, or undefined when out of bounds.

    • Mirrors String.prototype.charAt. The grapheme at index, or '' if out of bounds. Like native — and unlike at — a negative index is out of bounds rather than counted from the end.

      Parameters

      • index: number

        Grapheme index. Fractional truncates toward zero and NaN becomes 0.

      Returns string

      The grapheme cluster at index, or '' when out of bounds.

    • Mirrors String.prototype.codePointAt, indexed by grapheme. For a grapheme built from several code points this reports only the first one.

      Parameters

      • index: number

        Grapheme index. Fractional truncates toward zero and NaN becomes 0.

      Returns undefined | number

      The first code point of the grapheme at index, or undefined when out of bounds.

    • Mirrors String.prototype.endsWith: whether an occurrence of searchString ends exactly at endPosition (default: the end of the string). A negative endPosition clamps to 0 and an empty needle returns true. The match must begin on a grapheme boundary.

      Parameters

      • searchString: string | GraphemeString

        Needle to look for. Used raw and never segmented.

      • OptionalendPosition: number

        Grapheme index the match must end at. Defaults to the end of the string; negative clamps to 0.

      Returns boolean

      true if searchString ends exactly at endPosition and begins on a grapheme boundary. An empty needle returns true.

    • formatReplacementToArray with every part coerced to a string and joined.

      Parameters

      • replacers: object | { [key: string | number]: unknown }

        Map from key text to its replacement. A key absent from the map is replaced by the key text itself.

      Returns string

      The formatted string. '' if this string is empty. A replacer that cannot be converted becomes [object Object], or [object Unknown] if even that inspection throws.

      new GraphemeString('a{n}b').formatReplacement({ n: 9000 }); // 'a9000b'
      

      A replacer that cannot be converted to a string degrades to a placeholder rather than throwing, because the template is a localized string and the replacers are caller-supplied values — one bad value must not take down the whole call. Use formatReplacementToArray to keep a non-string replacer intact instead of coerced.

    • Replace each {key} in this string with replacers[key] and unescape \{/\}. An unknown key is replaced by the key text itself. Adjacent strings are concatenated, so a replacer that is not a string stays its own entry — which is how a React element survives being substituted in.

      No native counterpart, but it walks the string character by character, so it belongs here rather than beside the plain string helpers: an instance built once from a template can be formatted repeatedly without re-segmenting it.

      Type Parameters

      • T = unknown

      Parameters

      • replacers: object | { [key: string | number]: T }

        Map from key text to its replacement. A key absent from the map is replaced by the key text itself rather than treated as an error.

      Returns (string | T)[]

      The formatted parts in order. Adjacent strings are merged into one entry, so a non-string replacer is always its own entry. A template with no placeholders yields a single string entry; only the empty string yields an empty array.

      new GraphemeString('Hi, {name}! I like \\{curly braces\\}!').formatReplacementToArray({
      name: <b>Alice</b>,
      });
      // ['Hi, ', <b>Alice</b>, '! I like {curly braces}!']
    • Mirrors String.prototype.includes. See indexOf for position and boundary rules.

      Parameters

      • searchString: string | GraphemeString

        Needle to find. Used raw and never segmented.

      • Optionalposition: number

        Grapheme index to start from. Defaults to 0; negative clamps to 0.

      Returns boolean

      true if searchString occurs on grapheme boundaries at or after position. An empty needle returns true.

    • Mirrors String.prototype.indexOf: the first grapheme index at or after position where searchString occurs, or -1. A negative position clamps to 0, and an empty needle reports the clamped position itself. Only a hit that begins and ends on a grapheme boundary counts, so searching for a single emoji that forms part of a larger cluster reports -1 rather than matching inside it.

      Accepts a raw string or a GraphemeString; the needle is used raw and is never segmented.

      Parameters

      • searchString: string | GraphemeString

        Needle to find. Used raw and never segmented.

      • Optionalposition: number

        Grapheme index to start from. Defaults to 0; negative clamps to 0.

      Returns number

      The grapheme index of the first match, or -1 if there is none. An empty needle returns the clamped position.

    • Mirrors String.prototype.lastIndexOf: the last grapheme index at or before position where searchString occurs, or -1. As in native, an omitted or NaN position searches the whole string while a negative one clamps to 0. See indexOf for the boundary rule.

      Parameters

      • searchString: string | GraphemeString

        Needle to find. Used raw and never segmented.

      • Optionalposition: number

        Grapheme index to search at or before. Omitted or NaN searches the whole string; negative clamps to 0.

      Returns number

      The grapheme index of the last match, or -1 if there is none. An empty needle returns the clamped position.

    • Mirrors String.prototype.padEnd. See padStart, including the RangeError ceiling and why this returns text rather than a GraphemeString.

      Parameters

      • targetLength: number

        Desired length in graphemes.

      • OptionalpadString: string

        Text to repeat. Defaults to a single space.

      Returns string

      The padded text, or this instance's text unchanged when no padding is needed.

      RangeError when targetLength exceeds MAX_PADDING_LENGTH and padding would actually be added.

    • Mirrors String.prototype.padStart, choosing whole graphemes as filler so the result is targetLength graphemes long — where native fills UTF-16 slots and can leave a broken half of a cluster at the seam. Throws RangeError above MAX_PADDING_LENGTH, a lower ceiling than native's and the class's one deliberate departure from native behavior.

      Returns text rather than a GraphemeString, unlike the range methods. Those only ever remove clusters, so a range of an honest segmentation is still honest and can be carried into the result for free. Padding adds text at a seam, and added text can fuse with what is already there — a filler ending in a combining mark joins the character it lands against — so there is no segmentation to carry. Constructing one here would mean either re-segmenting on every call or handing back an instance whose cluster array disagrees with its own text.

      Parameters

      • targetLength: number

        Desired length in graphemes. No padding is added when it is at or below the current length.

      • OptionalpadString: string

        Text to repeat, truncated at a grapheme boundary. Defaults to a single space; an empty string adds no padding.

      Returns string

      The padded text, or this instance's text unchanged when no padding is needed.

      RangeError when targetLength exceeds MAX_PADDING_LENGTH and padding would actually be added. An empty padString never pads, so it never throws.

    • Mirrors String.prototype.slice. Negative indexes count back from the end and a backwards range yields an empty result.

      Parameters

      • OptionalindexStart: number

        First grapheme to include. Defaults to 0; negative counts back from the end.

      • OptionalindexEnd: number

        First grapheme to exclude. Defaults to the end; negative counts back from the end.

      Returns GraphemeString

      A new instance over [indexStart, indexEnd), reusing this instance's segmentation. Empty when the range is backwards or empty.

    • Mirrors String.prototype.split, including the parts that surprise people: a splitLimit discards everything past the limit rather than keeping it as a final piece, the limit is converted with ToUint32 (so -1 means "no limit" while NaN and Infinity mean "empty result"), an omitted separator yields the whole string, and a regular expression's capture groups are interleaved into the result.

      The grapheme substitutions: an empty separator splits into graphemes rather than UTF-16 units, and a separator only matches where it begins and ends on grapheme boundaries.

      PERF: an empty separator wraps every grapheme in its own instance. When the text is all that is wanted, toArray produces the same clusters as plain strings and skips that entirely.

      Entries are undefined exactly where native produces undefined — a capture group that did not participate in the match.

      Parameters

      • Optionalseparator: string | GraphemeString

        Literal string to split on, raw or as a GraphemeString. Omitted yields the whole string as a single piece; '' splits into individual graphemes.

      • OptionalsplitLimit: number

        Maximum number of entries to return, converted with ToUint32. Anything past the limit is discarded rather than kept as a final piece. Omitted means no limit.

      Returns GraphemeString[]

      The pieces in order. Empty when splitLimit resolves to 0. Never contains undefined — only a capture group can produce one, and a literal separator has none.

    • Splitting on a regular expression. See the string overload for the shared rules.

      Parameters

      • separator: RegExp

        Regular expression to split on. Its capture groups are interleaved into the result.

      • OptionalsplitLimit: number

        Maximum number of entries to return, converted with ToUint32.

      Returns (undefined | GraphemeString)[]

      The pieces in order. An entry is undefined exactly where a capture group did not participate in its match, as native does.

    • Splitting on a separator whose kind is not known statically — a string | RegExp union, which is what the free split in string-util declares. TypeScript matches a union argument against one overload at a time rather than distributing it, so without this a caller holding that union gets TS2769 and has to narrow at every call.

      Parameters

      • separator: string | RegExp | GraphemeString

        Literal string, GraphemeString, or regular expression to split on.

      • OptionalsplitLimit: number

        Maximum number of entries to return, converted with ToUint32.

      Returns (undefined | GraphemeString)[]

      The pieces in order. An entry can be undefined only when the separator turns out to be a regular expression with a capture group that did not participate.

    • Mirrors String.prototype.startsWith: whether an occurrence of searchString begins at position. A negative position clamps to 0 and an empty needle returns true. The match must end on a grapheme boundary, so a prefix ending mid-cluster is rejected.

      Parameters

      • searchString: string | GraphemeString

        Needle to look for. Used raw and never segmented.

      • Optionalposition: number

        Grapheme index the match must begin at. Defaults to 0; negative clamps to 0.

      Returns boolean

      true if searchString begins at position and ends on a grapheme boundary. An empty needle returns true.

    • Mirrors String.prototype.substring. Negative indexes clamp to 0 rather than counting from the end, and — as in native — the arguments are swapped when begin is greater than end.

      Parameters

      • Optionalbegin: number

        First grapheme to include. Defaults to 0; negative clamps to 0.

      • Optionalend: number

        First grapheme to exclude. Defaults to the end; negative clamps to 0.

      Returns GraphemeString

      A new instance over the range, reusing this instance's segmentation. Empty when begin and end resolve to the same index.

    • The grapheme clusters as an array. Returns a fresh copy, so mutating it cannot corrupt this instance. Equivalent to spreading this instance, and to spreading a native string except that native yields code points rather than clusters.

      Returns string[]

      A new array of the grapheme clusters, in order. Empty for the empty string.

    • The text, for JSON.stringify. Without this an instance would serialize its internals — the raw string and the grapheme array beside it — at roughly twice the size of the text, and the result would not read back as anything useful.

      Note that this makes serialization one-way: what comes off the wire is a plain string, not a GraphemeString. The class is a local segmentation cache rather than a transferable value, so a receiver that wants one constructs it from the text.

      Returns string

      The raw string this instance was built from, unchanged.

    • The original raw string. Named toString rather than exposed as a property so an instance drops straight into a template literal or String(...) without an accessor.

      Returns string

      The raw string this instance was built from, unchanged.