Skip to main content

xutf/
grapheme.rs

1//! Extended grapheme cluster segmentation (UAX #29, Unicode 17) with code-unit
2//! offsets and terminal cell width computed in the same pass. The permissive
3//! iterators allocate no memory; [`Graphemes`] also iterates from both ends and
4//! reports exact lengths.
5
6use core::{iter::FusedIterator, marker::PhantomData};
7
8use crate::{
9	encoding::Encoding,
10	props::{
11		CB_CONTROL, CB_CR, CB_EXTEND, CB_EXTEND_INCB_LINKER, CB_L, CB_LF, CB_LV, CB_LVT, CB_MASK,
12		CB_OTHER_INCB_CONSONANT, CB_PREPEND, CB_RI, CB_SPACING_MARK, CB_T, CB_V, CB_ZWJ, EPIC_BIT,
13		INCB_EXTEND_BIT, WIDTH_EMOJI_TEXT, WIDTH_SHIFT, is_emoji_modifier_base, props,
14	},
15	simd::plain_prefix,
16	unit::Unit,
17	utf8::Utf8,
18};
19
20/// A borrowed extended grapheme cluster and its terminal cell width.
21pub struct Grapheme<'a, E: Encoding> {
22	/// Code units belonging to this cluster.
23	pub units: &'a [E::Unit],
24	/// Terminal cells occupied by this cluster.
25	pub width: usize,
26}
27
28impl<E: Encoding> Clone for Grapheme<'_, E> {
29	#[inline(always)]
30	fn clone(&self) -> Self {
31		*self
32	}
33}
34
35impl<E: Encoding> Copy for Grapheme<'_, E> {}
36
37impl<E: Encoding> Grapheme<'_, E> {
38	/// Returns whether the cluster's base codepoint is a C0, DEL, or C1
39	/// control (`Cc`).
40	///
41	/// CRLF is one cluster and reports `true`. Controls always have width zero,
42	/// but the converse does not hold: zero-width formats such as ZWSP and ZWJ,
43	/// and combining marks, report `false`. Terminal renderers can use this
44	/// distinction to decide whether to skip a cluster or draw it. An empty
45	/// manually constructed cluster reports `false`.
46	#[inline]
47	pub fn is_control(&self) -> bool {
48		let mut units = self.units;
49		if units.is_empty() {
50			return false;
51		}
52		let cp = E::decode(&mut units);
53		matches!(cp, 0x00..=0x1f | 0x7f | 0x80..=0x9f)
54	}
55}
56
57/// Allocation-free double-ended iterator over extended grapheme clusters.
58///
59/// The length is exact: [`size_hint`](Iterator::size_hint) and
60/// [`len`](ExactSizeIterator::len) count the remaining clusters in one O(n)
61/// scan instead of returning cheap bounds.
62pub struct Graphemes<'a, E: Encoding> {
63	rest:      &'a [E::Unit],
64	_encoding: PhantomData<E>,
65}
66
67impl<E: Encoding> Clone for Graphemes<'_, E> {
68	#[inline(always)]
69	fn clone(&self) -> Self {
70		Self { rest: self.rest, _encoding: PhantomData }
71	}
72}
73
74impl<'a, E: Encoding> Iterator for Graphemes<'a, E> {
75	type Item = Grapheme<'a, E>;
76
77	#[inline]
78	fn next(&mut self) -> Option<Self::Item> {
79		if self.rest.is_empty() {
80			return None;
81		}
82		let scan = next_cluster::<E>(self.rest);
83		let (units, rest) = self.rest.split_at(scan.units);
84		self.rest = rest;
85		Some(Grapheme { units, width: scan.width })
86	}
87
88	#[inline]
89	fn size_hint(&self) -> (usize, Option<usize>) {
90		let len = cluster_count::<E>(self.rest);
91		(len, Some(len))
92	}
93
94	#[inline]
95	fn count(self) -> usize {
96		cluster_count::<E>(self.rest)
97	}
98
99	#[inline]
100	fn last(mut self) -> Option<Grapheme<'a, E>> {
101		self.next_back()
102	}
103}
104
105impl<'a, E: Encoding> DoubleEndedIterator for Graphemes<'a, E> {
106	#[inline]
107	fn next_back(&mut self) -> Option<Grapheme<'a, E>> {
108		if self.rest.is_empty() {
109			return None;
110		}
111		let scan = prev_cluster::<E>(self.rest);
112		let (rest, units) = self.rest.split_at(self.rest.len() - scan.units);
113		self.rest = rest;
114		Some(Grapheme { units, width: scan.width })
115	}
116}
117
118impl<E: Encoding> ExactSizeIterator for Graphemes<'_, E> {
119	#[inline]
120	fn len(&self) -> usize {
121		cluster_count::<E>(self.rest)
122	}
123}
124
125impl<E: Encoding> FusedIterator for Graphemes<'_, E> {}
126
127/// Allocation-free double-ended iterator over extended grapheme clusters and
128/// their code-unit offsets.
129///
130/// Offsets are measured from the start of the original input: bytes for UTF-8,
131/// `u16` units for UTF-16, and `u32` units for UTF-32. Empty input yields no
132/// items. Like [`Graphemes`], the length is exact at the cost of a counting
133/// scan.
134pub struct GraphemeIndices<'a, E: Encoding> {
135	inner:  Graphemes<'a, E>,
136	offset: usize,
137}
138
139impl<E: Encoding> Clone for GraphemeIndices<'_, E> {
140	#[inline(always)]
141	fn clone(&self) -> Self {
142		Self { inner: self.inner.clone(), offset: self.offset }
143	}
144}
145
146impl<'a, E: Encoding> Iterator for GraphemeIndices<'a, E> {
147	type Item = (usize, Grapheme<'a, E>);
148
149	#[inline]
150	fn next(&mut self) -> Option<Self::Item> {
151		let grapheme = self.inner.next()?;
152		let offset = self.offset;
153		self.offset += grapheme.units.len();
154		Some((offset, grapheme))
155	}
156
157	#[inline]
158	fn size_hint(&self) -> (usize, Option<usize>) {
159		self.inner.size_hint()
160	}
161
162	#[inline]
163	fn count(self) -> usize {
164		self.inner.count()
165	}
166
167	#[inline]
168	fn last(mut self) -> Option<Self::Item> {
169		self.next_back()
170	}
171}
172
173impl<E: Encoding> DoubleEndedIterator for GraphemeIndices<'_, E> {
174	#[inline]
175	fn next_back(&mut self) -> Option<Self::Item> {
176		let grapheme = self.inner.next_back()?;
177		Some((self.offset + self.inner.rest.len(), grapheme))
178	}
179}
180
181impl<E: Encoding> ExactSizeIterator for GraphemeIndices<'_, E> {
182	#[inline]
183	fn len(&self) -> usize {
184		self.inner.len()
185	}
186}
187
188impl<E: Encoding> FusedIterator for GraphemeIndices<'_, E> {}
189
190/// Iterates the extended grapheme clusters of an encoded slice with their
191/// code-unit offsets.
192///
193/// Offsets are bytes for UTF-8 and element counts for UTF-16 and UTF-32. Empty
194/// input yields no items.
195#[inline(always)]
196pub const fn grapheme_indices<E: Encoding>(input: &[E::Unit]) -> GraphemeIndices<'_, E> {
197	GraphemeIndices { inner: graphemes(input), offset: 0 }
198}
199
200/// Iterates the extended grapheme clusters of an encoded slice without
201/// allocating.
202#[inline(always)]
203pub const fn graphemes<E: Encoding>(input: &[E::Unit]) -> Graphemes<'_, E> {
204	Graphemes { rest: input, _encoding: PhantomData }
205}
206
207/// Double-ended, exact-length iterator over the extended grapheme clusters of
208/// a UTF-8 string, yielding borrowed sub-strings. Created by
209/// [`graphemes_str`].
210#[derive(Clone)]
211pub struct StrGraphemes<'a> {
212	inner: Graphemes<'a, Utf8>,
213}
214
215impl<'a> Iterator for StrGraphemes<'a> {
216	type Item = &'a str;
217
218	#[inline]
219	fn next(&mut self) -> Option<&'a str> {
220		// SAFETY: cluster boundaries fall on char boundaries in valid UTF-8.
221		self
222			.inner
223			.next()
224			.map(|g| unsafe { core::str::from_utf8_unchecked(g.units) })
225	}
226
227	#[inline]
228	fn size_hint(&self) -> (usize, Option<usize>) {
229		self.inner.size_hint()
230	}
231
232	#[inline]
233	fn count(self) -> usize {
234		self.inner.count()
235	}
236
237	#[inline]
238	fn last(mut self) -> Option<&'a str> {
239		self.next_back()
240	}
241}
242
243impl<'a> DoubleEndedIterator for StrGraphemes<'a> {
244	#[inline]
245	fn next_back(&mut self) -> Option<&'a str> {
246		// SAFETY: cluster boundaries fall on char boundaries in valid UTF-8.
247		self
248			.inner
249			.next_back()
250			.map(|g| unsafe { core::str::from_utf8_unchecked(g.units) })
251	}
252}
253
254impl ExactSizeIterator for StrGraphemes<'_> {
255	#[inline]
256	fn len(&self) -> usize {
257		self.inner.len()
258	}
259}
260
261impl FusedIterator for StrGraphemes<'_> {}
262
263/// Double-ended, exact-length iterator over a UTF-8 string's grapheme
264/// clusters and byte offsets.
265#[derive(Clone)]
266pub struct StrGraphemeIndices<'a> {
267	inner: GraphemeIndices<'a, Utf8>,
268}
269
270#[inline(always)]
271const fn indexed_str(item: (usize, Grapheme<'_, Utf8>)) -> (usize, &str) {
272	let (offset, grapheme) = item;
273	// SAFETY: cluster boundaries fall on char boundaries in valid UTF-8.
274	(offset, unsafe { core::str::from_utf8_unchecked(grapheme.units) })
275}
276
277impl<'a> Iterator for StrGraphemeIndices<'a> {
278	type Item = (usize, &'a str);
279
280	#[inline]
281	fn next(&mut self) -> Option<Self::Item> {
282		self.inner.next().map(indexed_str)
283	}
284
285	#[inline]
286	fn size_hint(&self) -> (usize, Option<usize>) {
287		self.inner.size_hint()
288	}
289
290	#[inline]
291	fn count(self) -> usize {
292		self.inner.count()
293	}
294
295	#[inline]
296	fn last(mut self) -> Option<Self::Item> {
297		self.next_back()
298	}
299}
300
301impl DoubleEndedIterator for StrGraphemeIndices<'_> {
302	#[inline]
303	fn next_back(&mut self) -> Option<Self::Item> {
304		self.inner.next_back().map(indexed_str)
305	}
306}
307
308impl ExactSizeIterator for StrGraphemeIndices<'_> {
309	#[inline]
310	fn len(&self) -> usize {
311		self.inner.len()
312	}
313}
314
315impl FusedIterator for StrGraphemeIndices<'_> {}
316
317/// Iterates the extended grapheme clusters of a UTF-8 string as borrowed
318/// strings.
319#[inline]
320pub const fn graphemes_str(input: &str) -> StrGraphemes<'_> {
321	StrGraphemes { inner: graphemes::<Utf8>(input.as_bytes()) }
322}
323
324/// Iterates a UTF-8 string's extended grapheme clusters with their byte
325/// offsets.
326///
327/// Each offset is a valid character boundary measured from the start of the
328/// input. Empty input yields no items.
329#[inline]
330pub const fn grapheme_indices_str(input: &str) -> StrGraphemeIndices<'_> {
331	StrGraphemeIndices { inner: grapheme_indices::<Utf8>(input.as_bytes()) }
332}
333
334/// One scanned cluster: code units consumed and terminal cell width.
335pub struct ClusterScan {
336	pub units: usize,
337	pub width: usize,
338}
339
340/// Standalone cell width encoded in a props byte (class 3 reads as 1).
341#[inline(always)]
342pub const fn width_value(p: u8) -> usize {
343	let width = (p >> WIDTH_SHIFT) & 3;
344	if width == WIDTH_EMOJI_TEXT {
345		1
346	} else {
347		width as usize
348	}
349}
350
351/// Incremental join state for one cluster, driving [`next_cluster`] and the
352/// flat scan inside [`crate::width`]: one decode and one table load per
353/// codepoint, no re-scanning at boundaries.
354pub struct ClusterState {
355	width:      usize,
356	prev:       u8,
357	prev_cp:    u32,
358	epic:       u8,
359	incb:       u8,
360	ri_odd:     bool,
361	after_zwj:  bool,
362	promotable: bool,
363	promote:    bool,
364}
365
366impl ClusterState {
367	/// Starts a cluster whose base is `cp0` with packed props `p0`.
368	#[inline(always)]
369	pub fn start(cp0: u32, p0: u8) -> Self {
370		let c0 = p0 & CB_MASK;
371		Self {
372			width:      width_value(p0),
373			prev:       c0,
374			prev_cp:    cp0,
375			epic:       u8::from(p0 & EPIC_BIT != 0),
376			incb:       u8::from(c0 == CB_OTHER_INCB_CONSONANT),
377			ri_odd:     c0 == CB_RI,
378			after_zwj:  false,
379			promotable: (p0 >> WIDTH_SHIFT) & 3 == WIDTH_EMOJI_TEXT,
380			promote:    false,
381		}
382	}
383
384	/// `true` when the cluster would absorb a following printable ASCII unit;
385	/// only a Prepend base does (`GB9b`).
386	#[inline(always)]
387	pub const fn joins_plain(&self) -> bool {
388		self.prev == CB_PREPEND
389	}
390
391	/// Tries to join `cp` (packed props `p`) onto the cluster, updating break
392	/// and width state when it joins.
393	#[inline(always)]
394	pub fn try_join(&mut self, cp: u32, p: u8) -> bool {
395		let c = p & CB_MASK;
396
397		if matches!(self.prev, CB_CR | CB_LF | CB_CONTROL) {
398			// GB3/GB4: controls break from everything except CR before LF.
399			if self.prev != CB_CR || c != CB_LF {
400				return false;
401			}
402		} else {
403			let join = match c {
404				CB_CR | CB_LF | CB_CONTROL => false,
405				CB_EXTEND | CB_EXTEND_INCB_LINKER | CB_ZWJ => true,
406				CB_SPACING_MARK => true,
407				_ if self.prev == CB_PREPEND => true,
408				CB_L => self.prev == CB_L,
409				CB_V => matches!(self.prev, CB_L | CB_LV | CB_V),
410				CB_T => matches!(self.prev, CB_LV | CB_V | CB_LVT | CB_T),
411				CB_LV | CB_LVT => self.prev == CB_L,
412				CB_RI => self.prev == CB_RI && self.ri_odd,
413				CB_OTHER_INCB_CONSONANT => self.incb == 2,
414				_ => self.prev == CB_ZWJ && self.epic == 2 && p & EPIC_BIT != 0,
415			};
416			if !join {
417				return false;
418			}
419		}
420
421		if c == CB_EXTEND || c == CB_EXTEND_INCB_LINKER {
422			if self.epic != 1 {
423				self.epic = 0;
424			}
425		} else if c == CB_ZWJ {
426			self.epic = if self.epic == 1 { 2 } else { 0 };
427		} else if p & EPIC_BIT != 0 {
428			self.epic = 1;
429		} else {
430			self.epic = 0;
431		}
432
433		if c == CB_OTHER_INCB_CONSONANT {
434			self.incb = 1;
435		} else if c == CB_EXTEND_INCB_LINKER {
436			self.incb = if self.incb != 0 { 2 } else { 0 };
437		} else if p & INCB_EXTEND_BIT == 0 {
438			self.incb = 0;
439		}
440
441		self.ri_odd = c == CB_RI && !self.ri_odd;
442
443		if cp == 0xfe0f || cp == 0x20e3 {
444			self.promote = true;
445		}
446		if c == CB_ZWJ {
447			self.after_zwj = true;
448		} else if !self.after_zwj {
449			// Extend-class marks are zero-width, except a few that carry
450			// intrinsic width (emoji skin-tone modifiers, Kirat Rai vowels).
451			// A modifier renders into an immediately preceding
452			// Emoji_Modifier_Base ("\u{1F44D}\u{1F3FD}" is 2 cells) and
453			// stands alone otherwise ("0\u{1F3FD}" is 3).
454			let modifier = (c == CB_EXTEND || c == CB_EXTEND_INCB_LINKER)
455				&& width_value(p) == 2
456				&& is_emoji_modifier_base(self.prev_cp);
457			if !modifier {
458				self.width += width_value(p);
459			}
460		}
461		self.prev_cp = cp;
462		self.prev = c;
463		true
464	}
465
466	/// Cluster width with any pending VS16/keycap promotion applied.
467	#[inline(always)]
468	pub fn finish(&self) -> usize {
469		if self.promote && self.promotable {
470			self.width.max(2)
471		} else {
472			self.width
473		}
474	}
475}
476
477/// Scans the cluster at the head of a non-empty encoded slice in one pass.
478#[inline]
479pub fn next_cluster<E: Encoding>(input: &[E::Unit]) -> ClusterScan {
480	if !E::FOREIGN {
481		let u0 = input[0].to_u32();
482		if u0 < 0x80 {
483			if u0 == 0x0d {
484				if input.len() > 1 && input[1].to_u32() == 0x0a {
485					return ClusterScan { units: 2, width: 0 };
486				}
487				return ClusterScan { units: 1, width: 0 };
488			}
489			if input.len() == 1 || input[1].to_u32() < 0x80 {
490				let width = usize::from((0x20..=0x7e).contains(&u0));
491				return ClusterScan { units: 1, width };
492			}
493		}
494	}
495
496	let mut rest = input;
497	let cp0 = E::decode(&mut rest);
498	let mut state = ClusterState::start(cp0, props(cp0));
499
500	while !rest.is_empty() {
501		let mut peek = rest;
502		let cp = E::decode(&mut peek);
503		if !state.try_join(cp, props(cp)) {
504			break;
505		}
506		rest = peek;
507	}
508
509	ClusterScan { units: input.len() - rest.len(), width: state.finish() }
510}
511
512/// Conservative pairwise join test for the backward scan: `true` when a
513/// codepoint with packed props `b` could join a cluster ending in props `a`
514/// under *some* preceding context. `false` therefore proves a break
515/// regardless of history; the history-dependent rules (regional-indicator
516/// parity, emoji ZWJ chains, `InCB` linkers) conservatively stay `true`.
517#[inline(always)]
518const fn may_join(a: u8, b: u8) -> bool {
519	let ca = a & CB_MASK;
520	let cb = b & CB_MASK;
521	if matches!(ca, CB_CR | CB_LF | CB_CONTROL) {
522		// GB3/GB4: controls break from everything except CR before LF.
523		return ca == CB_CR && cb == CB_LF;
524	}
525	match cb {
526		CB_CR | CB_LF | CB_CONTROL => false,
527		CB_EXTEND | CB_EXTEND_INCB_LINKER | CB_ZWJ | CB_SPACING_MARK => true,
528		_ if ca == CB_PREPEND => true,
529		CB_L => ca == CB_L,
530		CB_V => matches!(ca, CB_L | CB_LV | CB_V),
531		CB_T => matches!(ca, CB_LV | CB_V | CB_LVT | CB_T),
532		CB_LV | CB_LVT => ca == CB_L,
533		CB_RI => ca == CB_RI,
534		// GB9c: a consonant joins only when a linker chain is still open,
535		// which requires the previous codepoint to keep it open.
536		CB_OTHER_INCB_CONSONANT => {
537			ca == CB_EXTEND_INCB_LINKER || (a & INCB_EXTEND_BIT != 0 && ca != CB_OTHER_INCB_CONSONANT)
538		},
539		_ => ca == CB_ZWJ && b & EPIC_BIT != 0,
540	}
541}
542
543/// Scans the cluster at the end of a non-empty encoded slice.
544///
545/// Decodes backwards to the nearest boundary provable from a codepoint pair
546/// alone ([`may_join`]), then re-runs the forward scanner from there, so the
547/// forward state machine stays the single segmentation authority. Malformed
548/// input may cut differently than the forward direction, but progress and
549/// in-bounds cuts still hold.
550#[inline]
551pub fn prev_cluster<E: Encoding>(input: &[E::Unit]) -> ClusterScan {
552	if !E::FOREIGN {
553		let last = input[input.len() - 1].to_u32();
554		if last < 0x80 {
555			let prev = if input.len() > 1 {
556				input[input.len() - 2].to_u32()
557			} else {
558				0x80
559			};
560			if last == 0x0a && prev == 0x0d {
561				return ClusterScan { units: 2, width: 0 };
562			}
563			// Two adjacent ASCII units always break (GB9b Prepend and CR are
564			// the only absorbers before ASCII, and both are handled above).
565			if input.len() == 1 || prev < 0x80 {
566				let width = usize::from((0x20..=0x7e).contains(&last));
567				return ClusterScan { units: 1, width };
568			}
569		}
570	}
571
572	let mut back = input;
573	let mut after = props(E::decode_back(&mut back));
574	while !back.is_empty() {
575		let mut peek = back;
576		let p = props(E::decode_back(&mut peek));
577		if !may_join(p, after) {
578			break;
579		}
580		back = peek;
581		after = p;
582	}
583
584	// Forward re-scan from the guaranteed boundary at `back.len()`.
585	let mut at = back.len();
586	loop {
587		let scan = next_cluster::<E>(&input[at..]);
588		if at + scan.units == input.len() {
589			return scan;
590		}
591		at += scan.units;
592	}
593}
594
595/// Counts the extended grapheme clusters of an encoded slice in one pass,
596/// with a SIMD bulk path over printable ASCII runs.
597pub fn cluster_count<E: Encoding>(input: &[E::Unit]) -> usize {
598	let mut rest = input;
599	let mut count = 0;
600	while !rest.is_empty() {
601		if !E::FOREIGN {
602			let run = plain_prefix(rest);
603			if run == rest.len() {
604				return count + run;
605			}
606			// All but the run's last unit are whole clusters; the last may
607			// open a promotable or extending cluster, such as a keycap.
608			if run > 1 {
609				count += run - 1;
610				rest = &rest[run - 1..];
611			}
612		}
613		let scan = next_cluster::<E>(rest);
614		count += 1;
615		rest = &rest[scan.units..];
616	}
617	count
618}