find_cluster_break

Find the position of grapheme cluster breaks in a string

unicode
grapheme
cluster
break
moon add moonbit-community/find_cluster_break@0.1.2
Download zip
Version
0.1.2
License
MIT
Last updated
7 months ago
Downloads
18
README

#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