siglus_scene_vm/runtime/
string_semantics.rs1#[inline]
10pub fn ascii_lower(s: &str) -> String {
11 s.chars().map(|c| c.to_ascii_lowercase()).collect()
12}
13
14#[inline]
15pub fn ascii_upper(s: &str) -> String {
16 s.chars().map(|c| c.to_ascii_uppercase()).collect()
17}
18
19#[inline]
20pub fn utf16_len(s: &str) -> usize {
21 s.encode_utf16().count()
22}
23
24pub fn utf16_slice(s: &str, start: usize, len: Option<usize>) -> String {
25 let units: Vec<u16> = s.encode_utf16().collect();
26 let start = start.min(units.len());
27 let end = len
28 .map(|n| start.saturating_add(n).min(units.len()))
29 .unwrap_or(units.len());
30 String::from_utf16_lossy(&units[start..end])
31}
32
33pub fn utf16_left(s: &str, len: usize) -> String {
34 utf16_slice(s, 0, Some(len))
35}
36
37pub fn utf16_right(s: &str, len: usize) -> String {
38 let total = utf16_len(s);
39 utf16_slice(s, total.saturating_sub(len), None)
40}
41
42pub fn utf16_code_unit(s: &str, index: usize) -> Option<u16> {
43 s.encode_utf16().nth(index)
44}
45
46fn utf16_index_for_byte_offset(s: &str, byte_offset: usize) -> usize {
47 s.get(..byte_offset)
48 .map(utf16_len)
49 .unwrap_or_else(|| utf16_len(s))
50}
51
52pub fn search_ascii_case_insensitive(haystack: &str, needle: &str) -> Option<usize> {
53 let hay = ascii_lower(haystack);
56 let needle = ascii_lower(needle);
57 hay.find(&needle)
58 .map(|byte_offset| utf16_index_for_byte_offset(&hay, byte_offset))
59}
60
61pub fn rsearch_ascii_case_insensitive(haystack: &str, needle: &str) -> Option<usize> {
62 let hay = ascii_lower(haystack);
63 let needle = ascii_lower(needle);
64 hay.rfind(&needle)
65 .map(|byte_offset| utf16_index_for_byte_offset(&hay, byte_offset))
66}
67
68#[inline]
69pub fn is_hankaku(ch: char) -> bool {
70 ch.is_ascii() || matches!(ch as u32, 0x00A5 | 0x203E | 0xFF61..=0xFF9F)
73}
74
75#[inline]
76pub fn display_width_char(ch: char) -> usize {
77 if is_hankaku(ch) { 1 } else { 2 }
78}
79
80pub fn display_width(s: &str) -> usize {
81 s.chars().map(display_width_char).sum()
82}
83
84pub fn left_by_display_width(s: &str, limit: usize) -> String {
85 let mut width = 0usize;
86 let mut out = String::new();
87 for ch in s.chars() {
88 let w = display_width_char(ch);
89 if width + w > limit {
90 break;
91 }
92 width += w;
93 out.push(ch);
94 }
95 out
96}
97
98pub fn right_by_display_width(s: &str, limit: usize) -> String {
99 let mut width = 0usize;
100 let mut out = Vec::new();
101 for ch in s.chars().rev() {
102 let w = display_width_char(ch);
103 if width + w > limit {
104 break;
105 }
106 width += w;
107 out.push(ch);
108 }
109 out.into_iter().rev().collect()
110}
111
112pub fn mid_by_display_width(s: &str, start_width: usize, len_width: Option<usize>) -> String {
113 let mut width = 0usize;
114 let mut out = String::new();
115 let end_width = len_width.map(|len| start_width.saturating_add(len));
116 for ch in s.chars() {
117 let w = display_width_char(ch);
118 if width >= start_width {
119 width = width.saturating_add(w);
120 if end_width.is_some_and(|end| width > end) {
121 break;
122 }
123 out.push(ch);
124 } else {
125 width = width.saturating_add(w);
126 }
127 }
128 out
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn utf16_script_indexes_are_not_utf8_offsets() {
137 assert_eq!(search_ascii_case_insensitive("あいう", "う"), Some(2));
138 assert_eq!(utf16_len("A😀B"), 4);
139 assert_eq!(utf16_code_unit("A😀B", 1), Some(0xD83D));
140 assert_eq!(utf16_code_unit("A😀B", 2), Some(0xDE00));
141 }
142
143 #[test]
144 fn halfwidth_katakana_counts_as_one_column() {
145 assert_eq!(display_width("Aアあ"), 4);
146 }
147
148 #[test]
149 fn mid_len_uses_absolute_display_positions_like_tstr() {
150 assert_eq!(mid_by_display_width("あいう", 1, Some(2)), "");
151 assert_eq!(mid_by_display_width("あいう", 2, Some(2)), "い");
152 }
153}