Sign in

    grapheme

    Unicode grapheme cluster segmentation library for MoonBit (UAX #29)

    unicode
    grapheme
    segmentation
    text
    uax29
    cluster
    emoji
    Download zip
    Author
    Version
    0.10.4
    License
    MIT
    Last updated
    3 months ago
    Downloads
    26K

    #grapheme

    CI License: MIT Unicode 17.0.0 UAX #29 compliant

    "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ".length() returns 11 in MoonBit โ€” this library makes it return 1.

    #Overview

    MoonBit's String uses UTF-16 internal representation, so length() and str[i] operate at the UTF-16 code unit level. This library provides APIs for safely manipulating strings at the grapheme cluster level (the unit humans perceive as a single "character"), based on the default extended grapheme cluster rules in UAX #29 (Unicode Text Segmentation). Locale-specific tailored rules are not supported.

    • Zero dependencies
    • Backends: wasm-gc, wasm, js, native
    • Bundle size: wasm-gc ~23 KB / wasm ~27 KB / js ~59 KB / native ~56 KB
    • Random access and slicing (unique among grapheme segmentation libraries)

    LayerProblemSolution
    L1: UTF-16 encodingstr[i] operates at code unit levelMoonBit core iter()
    L2: Grapheme clusterComposite emoji span multiple code pointsThis library
    L3: Display widthFull-width / half-width display widthsrami3l/unicodewidth

    All GB rules (GB3-GB13, GB999) from Unicode 17.0.0 are implemented as a state machine, passing all 766 official test cases.

    #Install

    moon add kawaz/grapheme

    Add the dependency to your package's moon.pkg:

    import { "kawaz/grapheme", }

    #Usage

    // Correct grapheme cluster counting
    let family = @grapheme.graphemes("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ")
    println(family.length()) // 1

    // Split, access, and slice
    let view = @grapheme.graphemes("Hello๐Ÿ‡ฏ๐Ÿ‡ตWorld")
    println(view.length()) // 11 (H,e,l,l,o,๐Ÿ‡ฏ๐Ÿ‡ต,W,o,r,l,d)
    println(view[5].to_owned()) // "๐Ÿ‡ฏ๐Ÿ‡ต"
    println(view[1:3].to_string()) // "el" (slice โ€” GraphemeView::to_string)

    // Iteration
    for cluster in view {
    println(cluster)
    }

    // Lazy evaluation: fast when you only need the first few clusters
    let first = @grapheme.grapheme_iter("very long text...").head()

    graphemes() pre-scans the entire string to provide random access and slicing. O(n) full scan + O(k) memory (k = cluster count).

    grapheme_iter() starts in O(1) with no pre-scan. Use it when you only need the first N clusters.

    #API

    Note: == comparison is based on code point sequences. Unicode normalization (NFC/NFD) is not considered, so precomposed and decomposed forms of the same character are treated as different GraphemeViews.

    #Features

    • UAX #29 Grapheme Cluster Break state machine implementation
    • Extended_Pictographic property support
    • Composite emoji support (ZWJ sequences, flags, skin tone modifiers)
    • Published on mooncakes.io
    • Two-level lookup table for O(1) property determination
    • Safe access (get), Show/Eq/Hash traits, is_empty, to_string
    • Slice operations (view[1:3])
    • Extended iteration (rev_iter, iter2, grapheme_indices)
    • Lazy iterator grapheme_iter() โ€” up to 88x faster for early-break use cases

    #Performance

    Benchmark results (wasm-gc target, MoonBit 0.1.20260327):

    Inputgraphemes()grapheme_iter()
    ASCII 13 chars0.96 usโ€”
    ASCII 1,000 chars66 us77 us (full scan)
    Emoji ZWJ ร— 105.4 usโ€”
    Flags ร— 101.5 usโ€”
    CJK 67 chars4.9 us4.9 us (full scan)
    Mixed real world2.6 us2.8 us (full scan)
    First 10 only (1,000 chars)67 us (full scan)0.75 us

    grapheme_iter() starts without pre-scanning, making it up to 88x faster when only the first N clusters are needed.

    • gcb_category(): 10 ns (O(1) two-stage table lookup)
    • Slice operation: 10 ns (boundary array index adjustment only)

    #Unicode Version

    Target: Unicode 17.0.0

    #License

    MIT License - Yoshiaki Kawazu (@kawaz)

    GraphemeView

    pub struct GraphemeView {
    // private fields
    }

    A view type that holds Unicode grapheme cluster boundary information. Each grapheme cluster can be accessed as a zero-copy slice of the original String. Supports slicing via cluster_start and cluster_end indices into the boundaries array.
    impl Eq for GraphemeView

    GraphemeView::get

    fn GraphemeView::get(self : GraphemeView, i : Int) -> StringView?

    Returns the i-th grapheme cluster as an Option, or None if out of range.

    GraphemeView::grapheme_indices

    fn GraphemeView::grapheme_indices(self : GraphemeView) -> Iter[(Int, Int, StringView)]

    Iterates over grapheme clusters with their UTF-16 code unit offsets. Yields (start_offset, end_offset, cluster) for each grapheme cluster. Offsets are compatible with String indexing methods. Note: For sliced GraphemeViews, offsets refer to positions in the original source string.

    GraphemeView::is_empty

    fn GraphemeView::is_empty(self : GraphemeView) -> Bool

    Returns true if there are no grapheme clusters.

    GraphemeView::iter

    fn GraphemeView::iter(self : GraphemeView) -> Iter[StringView]

    Iterates over grapheme clusters in order.

    GraphemeView::iter2

    fn GraphemeView::iter2(self : GraphemeView) -> Iter2[Int, StringView]

    Iterates over grapheme clusters with their 0-based cluster indices (not byte offsets).

    GraphemeView::length

    fn GraphemeView::length(self : GraphemeView) -> Int

    Returns the number of grapheme clusters.

    GraphemeView::op_as_view

    fn GraphemeView::op_as_view(self : GraphemeView, start? : Int, end? : Int) -> GraphemeView

    Returns a sliced GraphemeView. Enables view[start:end] syntax. Negative start is clamped to 0. End beyond length is clamped to length. If end < start, an empty view is returned.

    GraphemeView::op_get

    fn GraphemeView::op_get(self : GraphemeView, i : Int) -> StringView

    Returns the i-th grapheme cluster as a StringView. Panics if i is not in the range 0 <= i < length().

    GraphemeView::rev_iter

    fn GraphemeView::rev_iter(self : GraphemeView) -> Iter[StringView]

    Iterates over grapheme clusters in reverse order.

    GraphemeView::to_string

    fn GraphemeView::to_string(self : GraphemeView) -> String

    Returns the string content of this view (may be a substring if sliced).

    grapheme_iter

    fn grapheme_iter(s : String) -> Iter[StringView]

    Returns a lazy iterator that yields grapheme clusters one at a time without preprocessing the entire string. Use this when you only need the first few clusters of a long string. For random access or repeated iteration, use graphemes() instead.

    graphemes

    fn graphemes(s : String) -> GraphemeView

    Returns a GraphemeView that splits the string into grapheme cluster units. Implements UAX #29 GB rules as a state machine. Note: This function scans the entire string upfront (O(n) preprocessing). For lazy evaluation, use grapheme_iter() instead.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    ยฉ 2026 mooncakes.io