use icu_segmenter::LineSegmenter;
thread_local! {
static SEGMENTER: LineSegmenter = super::with_data_provider(|provider| {
LineSegmenter::try_new_auto_with_buffer_provider(provider, Default::default()).expect(
"src/i18n/data/icu_data.blob is committed and regenerated by \
scripts/make_icu_data.sh; a load failure here means its MARKERS \
no longer cover icu_segmenter (SegmenterBreakLineV1, \
SegmenterBreakGraphemeClusterV1, SegmenterLstmAutoV1)",
)
});
}
pub fn break_offsets(text: &str) -> Vec<usize> {
if text.is_empty() {
return Vec::new();
}
SEGMENTER.with(|segmenter| segmenter.as_borrowed().segment_str(text).skip(1).collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn pieces(text: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut prev = 0;
for offset in break_offsets(text) {
out.push(&text[prev..offset]);
prev = offset;
}
out
}
#[test]
fn thai_breaks_between_words() {
assert_eq!(
pieces("ภาษาไทยเป็นภาษาที่ไม่มีการเว้นวรรค"),
[
"ภาษา",
"ไทย",
"เป็น",
"ภาษา",
"ที่",
"ไม่",
"มี",
"การ",
"เว้นวรรค"
],
);
}
#[test]
fn the_other_complex_scripts_have_models_too() {
for (script, text) in [
("Khmer", "ក្រុមហ៊ុនតូចមួយកំពុងធ្វើការ"),
("Lao", "ພາສາລາວບໍ່ມີການເວັ້ນວັກລະຫວ່າງຄໍາ"),
("Burmese", "မြန်မာဘာသာစကားသည်စကားလုံးများကိုကွာဟမှုမရှိဘဲရေးသားသည်"),
] {
let n = pieces(text).len();
assert!(
n > 1,
"{script}: no word boundaries found, got {n} piece(s)"
);
}
}
#[test]
fn japanese_keeps_punctuation_off_a_line_edge() {
assert_eq!(
pieces("日本語の文章。「引用」とか"),
["日", "本", "語", "の", "文", "章。", "「引", "用」", "と", "か"],
);
}
#[test]
fn ordinary_latin_text_breaks_exactly_where_it_used_to() {
assert_eq!(
pieces("The quick brown fox jumps over the lazy dog."),
["The ", "quick ", "brown ", "fox ", "jumps ", "over ", "the ", "lazy ", "dog."],
);
assert_eq!(pieces("Türöffner-Gerät"), ["Türöffner-", "Gerät"]);
assert_eq!(pieces("10:30–12:00 Uhr"), ["10:30–", "12:00 ", "Uhr"]);
assert_eq!(pieces("„Zitat“ und mehr"), ["„Zitat“ ", "und ", "mehr"]);
assert_eq!(pieces("e.g. i.e. etc."), ["e.g. ", "i.e. ", "etc."]);
assert_eq!(pieces("Datei_name.txt"), ["Datei_name.txt"]);
}
#[test]
fn a_hyphen_before_digits_is_not_a_break() {
assert_eq!(pieces("ID-001"), ["ID-001"]);
assert_eq!(pieces("DIN VDE 0100-600"), ["DIN ", "VDE ", "0100-600"]);
assert_eq!(pieces("Anlagen-freigabe"), ["Anlagen-", "freigabe"]);
}
#[test]
fn a_non_breaking_hyphen_holds_a_token_together() {
assert_eq!(pieces("ID\u{2011}001"), ["ID\u{2011}001"]);
}
#[test]
fn a_figure_dash_is_a_break_opportunity() {
assert_eq!(pieces("co\u{2012}operate"), ["co\u{2012}", "operate"]);
}
#[test]
fn slashes_and_em_dashes_gain_opportunities() {
assert_eq!(pieces("a/b/c"), ["a/", "b/", "c"]);
assert_eq!(
pieces("https://example.com/some/path"),
["https://", "example.com/", "some/", "path"],
);
assert_eq!(pieces("long—dash—text"), ["long", "—", "dash", "—", "text"],);
}
#[test]
fn a_tab_breaks_after_itself() {
assert_eq!(pieces("a\tb"), ["a\t", "b"]);
}
#[test]
fn empty_text_has_no_offsets() {
assert!(break_offsets("").is_empty());
}
#[test]
fn offsets_are_ascending_char_boundaries_ending_at_the_length() {
for text in [
"hello world",
"ภาษาไทยเป็นภาษา",
"日本語の文章。",
"e\u{301}x—y",
"x",
] {
let offsets = break_offsets(text);
assert_eq!(offsets.last(), Some(&text.len()), "{text:?}");
assert!(!offsets.contains(&0), "{text:?} must not report offset 0");
assert!(
offsets.windows(2).all(|w| w[0] < w[1]),
"{text:?} offsets must ascend: {offsets:?}",
);
for &o in &offsets {
assert!(text.is_char_boundary(o), "{text:?} offset {o}");
}
}
}
#[test]
fn a_combining_mark_is_never_cut_from_its_base() {
assert_eq!(pieces("e\u{301} x"), ["e\u{301} ", "x"]);
assert_eq!(pieces("e\u{301}"), ["e\u{301}"]);
}
}