find_cluster_break

    Find the position of grapheme cluster breaks in a string

    unicode
    grapheme
    cluster
    break
    Download zip
    Author
    Version
    0.1.2
    License
    MIT
    Last updated
    last year
    Downloads
    34

    #Find Cluster Break

    A MoonBit implementation of Unicode grapheme cluster break detection. This library finds the position of grapheme cluster breaks in a string, helping you properly handle complex Unicode text including emoji, accented characters, and other multi-codepoint sequences.

    #Overview

    This is a MoonBit port of the JavaScript library find-cluster-break by Marijn Haverbeke. It implements the Unicode Standard Annex #29 for grapheme cluster boundary detection, ensuring that visually perceived characters are treated as single units.

    A grapheme cluster is what users typically think of as a "character" - it could be a simple letter like 'a', an accented character like 'รฉ', or a complex emoji sequence like '๐Ÿ‘จโ€๐ŸŽค' (man singer emoji). This library helps you find the boundaries between these clusters.

    #Installation

    Add this package to your MoonBit project:

    moon add hackwaly/find_cluster_break

    Then import it in your moon.pkg.json:

    { "import": ["hackwaly/find_cluster_break"] }

    #API Reference

    #find_cluster_break

    The main function for finding grapheme cluster breaks:

    test "basic usage" {
    // Find the next cluster break after position 0
    let result = @find_cluster_break.find_cluster_break("๐Ÿ’ช๐Ÿฝ๐Ÿฆ‹", 0)
    inspect(result, content="4") // Points to start of butterfly emoji
    }

    test "find all cluster breaks" {
    let text = "๐Ÿ‘จโ€๐ŸŽค๐Ÿ’ช๐Ÿฝ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"
    let breaks : Array[Int] = []
    let mut pos = 0

    while pos < text.length() {
    let next_break = @find_cluster_break.find_cluster_break(text, pos)
    if next_break == text.length() {
    break
    }
    breaks.push(next_break)
    pos = next_break
    }

    inspect(breaks, content="[5, 9]") // Positions of cluster boundaries
    }

    You can search for cluster breaks in both directions:

    test "directional search" {
    let text = "a๐Ÿ‘จโ€๐ŸŽคb"

    // Forward search (default)
    let forward = @find_cluster_break.find_cluster_break(text, 0, forward=true)
    inspect(forward, content="1")

    // Backward search
    let backward = @find_cluster_break.find_cluster_break(text, text.length(), forward=false)
    inspect(backward, content="6") // Start of last cluster
    }

    #Including Extending Characters

    Control whether extending characters are considered part of clusters:

    test "extending characters" {
    let text = "รฉฬ " // 'e' + combining grave + combining left angle below

    // With extending characters (default)
    let with_extending = @find_cluster_break.find_cluster_break(text, 0, include_extending=true)
    inspect(with_extending, content="2") // Treats the whole sequence as one cluster

    // Without extending characters
    let without_extending = @find_cluster_break.find_cluster_break(text, 0, include_extending=false)
    inspect(without_extending, content="1") // Only the base 'e'
    }

    #is_extending_char

    Check if a Unicode code point is an extending character:

    test "extending character detection" {
    // Combining grave accent (U+0300)
    let is_combining = @find_cluster_break.is_extending_char(768)
    inspect(is_combining, content="true")

    // Regular letter 'A'
    let is_letter = @find_cluster_break.is_extending_char(65)
    inspect(is_letter, content="false")

    // Skin tone modifier (U+1F3FD)
    let is_modifier = @find_cluster_break.is_extending_char(0x1F3FD)
    inspect(is_modifier, content="true")
    }

    #Complex Text Handling

    #Emoji Sequences

    The library correctly handles complex emoji sequences:

    test "emoji sequences" {
    // Man singer: man + ZWJ + microphone
    let singer = "๐Ÿ‘จโ€๐ŸŽค"
    let break_pos = @find_cluster_break.find_cluster_break(singer, 0)
    inspect(break_pos, content="5") // Entire sequence is one cluster

    // Flexed bicep with skin tone
    let flexed = "๐Ÿ’ช๐Ÿฝ"
    let muscle_break = @find_cluster_break.find_cluster_break(flexed, 0)
    inspect(muscle_break, content="4") // Emoji + skin tone modifier
    }

    #Regional Indicator Sequences (Flags)

    Flag emoji are correctly handled as pairs:

    test "flag emoji" {
    let flags = "๐Ÿ‡ฉ๐Ÿ‡ช๐Ÿ‡ซ๐Ÿ‡ท๐Ÿ‡ช๐Ÿ‡ธ" // German, French, Spanish flags
    let breaks : Array[Int] = []
    let mut pos = 0

    while pos < flags.length() {
    let next = @find_cluster_break.find_cluster_break(flags, pos)
    if next == flags.length() {
    break
    }
    breaks.push(next)
    pos = next
    }

    inspect(breaks, content="[4, 8]") // Each flag is 4 bytes (2 regional indicators)
    }

    #Accented Characters

    Handles both precomposed and decomposed accented characters:

    test "accented characters" {
    // Decomposed: 'e' + combining acute accent
    let decomposed = "รฉ"
    let d_break = @find_cluster_break.find_cluster_break(decomposed, 0)
    inspect(d_break, content="1") // Treats as single cluster

    // Complex accents: 'o' + multiple combining marks
    let complex = "ล‘ฬ " // o + double acute + left angle below
    let c_break = @find_cluster_break.find_cluster_break(complex, 0)
    inspect(c_break, content="2") // All combining marks included
    }

    #Character Counting

    Use this library to count visual characters correctly:

    test "visual character counting" {
    fn count_grapheme_clusters(text : String) -> Int {
    let mut count = 0
    let mut pos = 0

    while pos < text.length() {
    let next = @find_cluster_break.find_cluster_break(text, pos)
    if next == pos {
    break
    }
    count = count + 1
    pos = next
    }

    count
    }

    let text = "Hello ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ World!"
    let cluster_count = count_grapheme_clusters(text)
    inspect(cluster_count, content="14") // Visual characters, not code units

    // Compare with string length (code units)
    let code_unit_length = text.length()
    inspect(code_unit_length, content="24") // Much larger due to emoji
    }

    #Text Processing

    #Substring Extraction

    Extract substrings by grapheme clusters:

    test "cluster-aware substring" {
    fn substring_by_clusters(text : String, start : Int, length : Int) -> String {
    let mut current_cluster = 0
    let mut pos = 0
    let mut start_pos = 0
    let mut end_pos = text.length()

    // Find start position
    while current_cluster < start && pos < text.length() {
    let next = @find_cluster_break.find_cluster_break(text, pos)
    if next == pos {
    break
    }
    current_cluster = current_cluster + 1
    pos = next
    if current_cluster == start {
    start_pos = pos
    }
    }

    // Find end position
    let mut remaining = length
    while remaining > 0 && pos < text.length() {
    let next = @find_cluster_break.find_cluster_break(text, pos)
    if next == pos {
    break
    }
    remaining = remaining - 1
    pos = next
    if remaining == 0 {
    end_pos = pos
    }
    }

    text.substring(start=start_pos, end=end_pos)
    }

    let text = "๐Ÿš€โœจ๐ŸŽฏ"
    let sub = substring_by_clusters(text, 1, 1) // Get second cluster
    inspect(sub, content="โœจ") // Just the sparkles emoji
    }

    #License

    MIT License - see the LICENSE file for details.

    This is a MoonBit port of the JavaScript library find-cluster-break. The original implementation and algorithm design credit goes to Marijn Haverbeke.

    For more information about Unicode grapheme clusters, see:

    find_cluster_break

    fn find_cluster_break(str : String, pos : Int, forward? : Bool, include_extending? : Bool) -> Int

    Returns a next grapheme cluster break after (not equal to) pos, if forward is true, or before otherwise. Returns pos itself if no further cluster break is available in the string. Moves across surrogate pairs, extending characters (when include_extending is true, which is the default), characters joined with zero-width joiners, and flag emoji.

    is_extending_char

    fn is_extending_char(code : Int) -> Bool

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    ยฉ 2026 mooncakes.io