1use ropey::{RopeSlice, iter::Chunks};
6use std::iter::FusedIterator;
7use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
8
9#[must_use]
10pub fn is_char_boundary(rope: &RopeSlice, byte_offset: usize) -> bool {
11 assert!(
12 byte_offset <= rope.len(),
13 "byte offset {byte_offset} is out of bounds (rope length is {})",
14 rope.len()
15 );
16 if byte_offset == 0 || byte_offset == rope.len() {
17 return true;
18 }
19 let (chunk, chunk_byte_offset) = rope.chunk(byte_offset);
20 chunk.is_char_boundary(byte_offset - chunk_byte_offset)
21}
22
23#[must_use]
24pub fn floor_char_boundary(rope: &RopeSlice, byte_offset: usize) -> usize {
25 assert!(
26 byte_offset <= rope.len(),
27 "byte offset {byte_offset} is out of bounds (rope length is {})",
28 rope.len()
29 );
30 if byte_offset == rope.len() {
31 return byte_offset;
32 }
33 let (chunk, chunk_byte_offset) = rope.chunk(byte_offset);
34 chunk_byte_offset + chunk.floor_char_boundary(byte_offset - chunk_byte_offset)
35}
36
37#[must_use]
38pub fn ceil_char_boundary(rope: &RopeSlice, byte_offset: usize) -> usize {
39 assert!(
40 byte_offset <= rope.len(),
41 "byte offset {byte_offset} is out of bounds (rope length is {})",
42 rope.len()
43 );
44 if byte_offset == rope.len() {
45 return byte_offset;
46 }
47 let (chunk, chunk_byte_offset) = rope.chunk(byte_offset);
48 chunk_byte_offset + chunk.ceil_char_boundary(byte_offset - chunk_byte_offset)
49}
50
51#[must_use]
52pub fn is_grapheme_boundary(rope: &RopeSlice, byte_offset: usize) -> bool {
53 assert!(
54 byte_offset <= rope.len(),
55 "byte offset {byte_offset} is out of bounds (rope length is {})",
56 rope.len()
57 );
58 if !is_char_boundary(rope, byte_offset) {
59 return false;
60 }
61 let (chunk, chunk_byte_offset) = rope.chunk(byte_offset);
62 let mut cursor = GraphemeCursor::new(byte_offset, rope.len(), true);
63 loop {
64 match cursor.is_boundary(chunk, chunk_byte_offset) {
65 Ok(is_boundary) => return is_boundary,
66 Err(GraphemeIncomplete::PreContext(byte_offset)) => {
67 let (prev_chunk, prev_chunk_byte_offset) = rope.chunk(byte_offset - 1);
68 cursor.provide_context(prev_chunk, prev_chunk_byte_offset);
69 }
70 _ => unreachable!(),
71 }
72 }
73}
74
75#[must_use]
76pub fn is_grapheme_start(rope: &RopeSlice, byte_index: usize) -> bool {
77 assert!(
78 byte_index <= rope.len(),
79 "byte index {byte_index} is out of bounds (rope length is {})",
80 rope.len()
81 );
82 if byte_index == rope.len() {
83 return false;
84 }
85 is_grapheme_boundary(rope, byte_index)
86}
87
88#[must_use]
89pub fn prev_grapheme_boundary(rope: &RopeSlice, byte_offset: usize) -> Option<usize> {
90 assert!(
91 byte_offset <= rope.len(),
92 "byte offset {byte_offset} is out of bounds (rope length is {})",
93 rope.len()
94 );
95 if byte_offset == 0 {
96 return None;
97 }
98 let start = floor_char_boundary(rope, byte_offset);
100 if start < byte_offset && is_grapheme_boundary(rope, start) {
101 return Some(start);
102 }
103 let (mut chunk, mut chunk_byte_offset) = rope.chunk(start);
104 let mut cursor = GraphemeCursor::new(start, rope.len(), true);
105 loop {
106 match cursor.prev_boundary(chunk, chunk_byte_offset) {
107 Ok(boundary) => return boundary,
108 Err(GraphemeIncomplete::PrevChunk) => {
109 (chunk, chunk_byte_offset) = rope.chunk(chunk_byte_offset - 1);
110 }
111 Err(GraphemeIncomplete::PreContext(byte_offset)) => {
112 let (prev_chunk, prev_chunk_byte_offset) = rope.chunk(byte_offset - 1);
113 cursor.provide_context(prev_chunk, prev_chunk_byte_offset);
114 }
115 _ => unreachable!(),
116 }
117 }
118}
119
120#[must_use]
121pub fn next_grapheme_boundary(rope: &RopeSlice, byte_offset: usize) -> Option<usize> {
122 assert!(
123 byte_offset <= rope.len(),
124 "byte offset {byte_offset} is out of bounds (rope length is {})",
125 rope.len()
126 );
127 if byte_offset == rope.len() {
128 return None;
129 }
130 let start = ceil_char_boundary(rope, byte_offset);
132 if start > byte_offset && is_grapheme_boundary(rope, start) {
133 return Some(start);
134 }
135 let (mut chunk, mut chunk_byte_offset) = rope.chunk(start);
136 let mut cursor = GraphemeCursor::new(start, rope.len(), true);
137 loop {
138 match cursor.next_boundary(chunk, chunk_byte_offset) {
139 Ok(boundary) => return boundary,
140 Err(GraphemeIncomplete::NextChunk) => {
141 (chunk, chunk_byte_offset) = rope.chunk(chunk_byte_offset + chunk.len());
142 }
143 Err(GraphemeIncomplete::PreContext(byte_offset)) => {
144 let (prev_chunk, prev_chunk_byte_offset) = rope.chunk(byte_offset - 1);
145 cursor.provide_context(prev_chunk, prev_chunk_byte_offset);
146 }
147 _ => unreachable!(),
148 }
149 }
150}
151
152#[must_use]
153pub fn floor_grapheme_boundary(rope: &RopeSlice, byte_offset: usize) -> usize {
154 assert!(
155 byte_offset <= rope.len(),
156 "byte offset {byte_offset} is out of bounds (rope length is {})",
157 rope.len()
158 );
159 let byte_offset = floor_char_boundary(rope, byte_offset);
160 if is_grapheme_boundary(rope, byte_offset) {
161 return byte_offset;
162 }
163 prev_grapheme_boundary(rope, byte_offset)
164 .expect("only fails at start of text, which is a grapheme boundary")
165}
166
167#[must_use]
168pub fn ceil_grapheme_boundary(rope: &RopeSlice, byte_offset: usize) -> usize {
169 assert!(
170 byte_offset <= rope.len(),
171 "byte offset {byte_offset} is out of bounds (rope length is {})",
172 rope.len()
173 );
174 let byte_offset = ceil_char_boundary(rope, byte_offset);
175 if is_grapheme_boundary(rope, byte_offset) {
176 return byte_offset;
177 }
178 next_grapheme_boundary(rope, byte_offset)
179 .expect("only fails at end of text, which is a grapheme boundary")
180}
181
182#[derive(Clone, Debug)]
183pub struct Graphemes<'a> {
184 rope: RopeSlice<'a>,
185 chunks: Chunks<'a>,
186 chunk: &'a str,
187 chunk_byte_offset: usize,
188 cursor: GraphemeCursor,
189}
190
191impl<'a> Graphemes<'a> {
192 #[must_use]
193 pub fn new(rope: &RopeSlice<'a>) -> Self {
194 let mut chunks = rope.chunks();
195 let chunk = chunks.next().unwrap_or("");
196 Graphemes {
197 rope: *rope,
198 chunks,
199 chunk,
200 chunk_byte_offset: 0,
201 cursor: GraphemeCursor::new(0, rope.len(), true),
202 }
203 }
204}
205
206impl<'a> Iterator for Graphemes<'a> {
207 type Item = RopeSlice<'a>;
208
209 fn next(&mut self) -> Option<RopeSlice<'a>> {
210 let start = self.cursor.cur_cursor();
211 let end;
212 loop {
213 match self
214 .cursor
215 .next_boundary(self.chunk, self.chunk_byte_offset)
216 {
217 Ok(None) => return None,
218 Ok(Some(boundary)) => {
219 end = boundary;
220 break;
221 }
222 Err(GraphemeIncomplete::NextChunk) => {
223 self.chunk_byte_offset += self.chunk.len();
224 self.chunk = self
225 .chunks
226 .next()
227 .expect("only requested when the cursor is not at the end of the rope");
228
229 let crossing = self.cursor.cur_cursor();
232 self.cursor = GraphemeCursor::new(crossing, self.rope.len(), true);
233 if crossing > start && is_grapheme_boundary(&self.rope, crossing) {
234 end = crossing;
235 break;
236 }
237 }
239 Err(GraphemeIncomplete::PreContext(byte_offset)) => {
240 let (prev_chunk, prev_chunk_byte_offset) = self.rope.chunk(byte_offset - 1);
241 self.cursor
242 .provide_context(prev_chunk, prev_chunk_byte_offset);
243 }
244 _ => unreachable!(),
245 }
246 }
247 if start < self.chunk_byte_offset {
248 Some(self.rope.slice(start..end))
250 } else {
251 let start_in_chunk = start - self.chunk_byte_offset;
253 let end_in_chunk = end - self.chunk_byte_offset;
254 Some((&self.chunk[start_in_chunk..end_in_chunk]).into())
255 }
256 }
257
258 fn size_hint(&self) -> (usize, Option<usize>) {
259 let remaining_bytes = self.rope.len() - self.cursor.cur_cursor();
261 (usize::from(remaining_bytes > 0), Some(remaining_bytes))
262 }
263}
264
265impl FusedIterator for Graphemes<'_> {}
266
267#[cfg(test)]
281mod tests {
282 use super::*;
283 use hegel::{TestCase, generators as gs};
284 use ropey::Rope;
285 use unicode_segmentation::UnicodeSegmentation as _;
286
287 #[test]
289 fn test_flag_run_across_chunks() {
290 let text = "\u{1f1e6}\u{1f1e7}".repeat(1024 / 8 + 1); let rope = Rope::from_str(&text);
292 for start in [0, 4] {
293 let expected: Vec<&str> = text[start..].graphemes(true).collect();
294 let actual: Vec<String> = Graphemes::new(&rope.slice(start..))
295 .map(|grapheme| grapheme.to_string())
296 .collect();
297 assert_eq!(actual, expected, "slice starting at {start}");
298 }
299 }
300
301 #[hegel::test(test_cases = 10_000)]
302 fn test_is_char_boundary(tc: TestCase) {
303 let string = tc.draw(gs::text());
304 let input = tc.draw(gs::integers().max_value(string.len()));
305 let ropey = {
306 let rope = ropey::Rope::from(string.as_str());
307 is_char_boundary(&rope.slice(..), input)
308 };
309 let crop = {
310 let rope = crop::Rope::from(string.as_str());
311 rope.is_char_boundary(input)
312 };
313 assert_eq!(ropey, crop);
314 }
315
316 #[hegel::test(test_cases = 10_000)]
317 fn test_is_grapheme_boundary(tc: TestCase) {
318 let string = tc.draw(gs::text());
319 let input = tc.draw(gs::integers().max_value(string.len()));
320 let ropey = {
321 let rope = ropey::Rope::from(string.as_str());
322 is_grapheme_boundary(&rope.slice(..), input)
323 };
324 let crop = {
325 let rope = crop::Rope::from(string.as_str());
326 rope.is_grapheme_boundary(input)
327 };
328 assert_eq!(ropey, crop);
329 }
330
331 #[hegel::test(test_cases = 10_000)]
332 fn test_graphemes(tc: TestCase) {
333 let string = tc.draw(gs::text());
334 let ropey = {
335 let rope = ropey::Rope::from(string.as_str());
336 Graphemes::new(&rope.slice(..))
337 .map(|slice| slice.to_string())
338 .collect::<Vec<_>>()
339 };
340 let str = string
341 .graphemes(true)
342 .map(str::to_string)
343 .collect::<Vec<_>>();
344 assert_eq!(ropey, str);
345 }
346}