cosmic_text/
bidi_para.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use alloc::vec::Vec;
4use unicode_bidi::{bidi_class, BidiClass, BidiInfo, ParagraphInfo};
5
6/// An iterator over the paragraphs in the input text.
7/// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour.
8#[derive(Debug)]
9pub struct BidiParagraphs<'text> {
10    text: &'text str,
11    info: alloc::vec::IntoIter<ParagraphInfo>,
12}
13
14impl<'text> BidiParagraphs<'text> {
15    /// Create an iterator with optimized paragraph detection.
16    /// This version avoids `BidiInfo` allocation for simple ASCII text.
17    pub fn new(text: &'text str) -> Self {
18        // Fast path for simple ASCII text - just split on newlines
19        if text.is_ascii()
20            && !text
21                .chars()
22                .any(|c| c.is_ascii_control() && c != '\n' && c != '\r' && c != '\t')
23        {
24            // For simple ASCII, we can avoid `BidiInfo` entirely
25            // Create minimal ParagraphInfo entries for each line
26            let mut paragraphs = Vec::new();
27            let mut start = 0;
28
29            for (i, c) in text.char_indices() {
30                if c == '\n' {
31                    paragraphs.push(ParagraphInfo {
32                        range: start..i,
33                        level: unicode_bidi::Level::ltr(),
34                    });
35                    start = i + 1;
36                }
37            }
38
39            // Add final paragraph if text doesn't end with newline
40            if start < text.len() {
41                paragraphs.push(ParagraphInfo {
42                    range: start..text.len(),
43                    level: unicode_bidi::Level::ltr(),
44                });
45            }
46
47            let info = paragraphs.into_iter();
48            Self { text, info }
49        } else {
50            // Complex text - fall back to full `BidiInfo` analysis
51            let info = BidiInfo::new(text, None);
52            let info = info.paragraphs.into_iter();
53            Self { text, info }
54        }
55    }
56}
57
58impl<'text> Iterator for BidiParagraphs<'text> {
59    type Item = &'text str;
60
61    fn next(&mut self) -> Option<Self::Item> {
62        let para = self.info.next()?;
63        let paragraph = &self.text[para.range];
64        // `para.range` includes the newline that splits the line, so remove it if present
65        let mut char_indices = paragraph.char_indices();
66        char_indices
67            .next_back()
68            .and_then(|(i, c)| {
69                // `BidiClass::B` is a Paragraph_Separator (various newline characters)
70                (bidi_class(c) == BidiClass::B).then_some(i)
71            })
72            .map_or(Some(paragraph), |i| Some(&paragraph[0..i]))
73    }
74}