unic_bidi/
bidi_info.rs

1// Copyright 2015 The Servo Project Developers.
2// Copyright 2017 The UNIC Project Developers.
3//
4// See the COPYRIGHT file at the top-level directory of this distribution.
5//
6// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
7// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
8// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
9// option. This file may not be copied, modified, or distributed
10// except according to those terms.
11
12use std::borrow::Cow;
13use std::cmp::{max, min};
14use std::fmt;
15use std::iter::repeat;
16use std::ops::Range;
17
18use unic_ucd_bidi::bidi_class::abbr_names::*;
19use unic_ucd_bidi::BidiClass;
20
21use crate::explicit;
22use crate::format_chars;
23use crate::implicit;
24use crate::level;
25use crate::prepare;
26
27use crate::level::{Level, LTR_LEVEL, RTL_LEVEL};
28use crate::prepare::LevelRun;
29
30/// Bidi information about a single paragraph
31#[derive(Clone, Debug, Eq, PartialEq, Hash)]
32pub struct ParagraphInfo {
33    /// The paragraphs boundaries within the text, as byte indices.
34    ///
35    /// TODO: Shrink this to only include the starting index?
36    pub range: Range<usize>,
37
38    /// The paragraph embedding level.
39    ///
40    /// <https://www.unicode.org/reports/tr9/#BD4>
41    pub level: Level,
42}
43
44/// Initial bidi information of the text
45///
46/// Contains the paragraphs and `BidiClass`es in a string of text.
47#[derive(Clone, Debug, Eq, PartialEq, Hash)]
48pub struct InitialInfo<'text> {
49    /// The text
50    pub text: &'text str,
51
52    /// The BidiClass of the character at each byte in the text.
53    /// If a character is multiple bytes, its class will appear multiple times in the vector.
54    pub original_classes: Vec<BidiClass>,
55
56    /// The boundaries and level of each paragraph within the text.
57    pub paragraphs: Vec<ParagraphInfo>,
58}
59
60impl<'text> InitialInfo<'text> {
61    /// Find the paragraphs and `BidiClass`es in a string of text.
62    ///
63    /// <https://www.unicode.org/reports/tr9/#The_Paragraph_Level>
64    ///
65    /// Also sets the class for each First Strong Isolate initiator (FSI) to LRI or RLI if a strong
66    /// character is found before the matching PDI.  If no strong character is found, the class will
67    /// remain FSI, and it's up to later stages to treat these as LRI when needed.
68    pub fn new(text: &str, default_para_level: Option<Level>) -> InitialInfo<'_> {
69        let mut original_classes = Vec::with_capacity(text.len());
70
71        // The stack contains the starting byte index for each nested isolate we're inside.
72        let mut isolate_stack = Vec::new();
73        let mut paragraphs = Vec::new();
74
75        let mut para_start = 0;
76        let mut para_level = default_para_level;
77
78        for (i, c) in text.char_indices() {
79            let class = BidiClass::of(c);
80            original_classes.extend(repeat(class).take(c.len_utf8()));
81            match class {
82                B => {
83                    // P1. Split the text into separate paragraphs. The paragraph separator is kept
84                    // with the previous paragraph.
85                    let para_end = i + c.len_utf8();
86                    paragraphs.push(ParagraphInfo {
87                        range: para_start..para_end,
88                        // P3. If no character is found in p2, set the paragraph level to zero.
89                        level: para_level.unwrap_or(LTR_LEVEL),
90                    });
91                    // Reset state for the start of the next paragraph.
92                    para_start = para_end;
93                    // TODO: Support defaulting to direction of previous paragraph
94                    //
95                    // <https://www.unicode.org/reports/tr9/#HL1>
96                    para_level = default_para_level;
97                    isolate_stack.clear();
98                }
99                L | R | AL => {
100                    match isolate_stack.last() {
101                        Some(&start) => {
102                            if original_classes[start] == FSI {
103                                // X5c. If the first strong character between FSI and its matching
104                                // PDI is R or AL, treat it as RLI. Otherwise, treat it as LRI.
105                                for j in 0..format_chars::FSI.len_utf8() {
106                                    original_classes[start + j] =
107                                        if class == L { LRI } else { RLI };
108                                }
109                            }
110                        }
111                        None => {
112                            if para_level.is_none() {
113                                // P2. Find the first character of type L, AL, or R, while skipping
114                                // any characters between an isolate initiator and its matching
115                                // PDI.
116                                para_level = Some(if class != L { RTL_LEVEL } else { LTR_LEVEL });
117                            }
118                        }
119                    }
120                }
121                RLI | LRI | FSI => {
122                    isolate_stack.push(i);
123                }
124                PDI => {
125                    isolate_stack.pop();
126                }
127                _ => {}
128            }
129        }
130        if para_start < text.len() {
131            paragraphs.push(ParagraphInfo {
132                range: para_start..text.len(),
133                level: para_level.unwrap_or(LTR_LEVEL),
134            });
135        }
136        assert_eq!(original_classes.len(), text.len());
137
138        InitialInfo {
139            text,
140            original_classes,
141            paragraphs,
142        }
143    }
144}
145
146/// Bidi information of the text
147///
148/// The `original_classes` and `levels` vectors are indexed by byte offsets into the text.  If a
149/// character is multiple bytes wide, then its class and level will appear multiple times in these
150/// vectors.
151// TODO: Impl `struct StringProperty<T> { values: Vec<T> }` and use instead of Vec<T>
152#[derive(Debug, Eq, PartialEq, Hash)]
153pub struct BidiInfo<'text> {
154    /// The text
155    pub text: &'text str,
156
157    /// The BidiClass of the character at each byte in the text.
158    pub original_classes: Vec<BidiClass>,
159
160    /// The directional embedding level of each byte in the text.
161    pub levels: Vec<Level>,
162
163    /// The boundaries and paragraph embedding level of each paragraph within the text.
164    ///
165    /// TODO: Use SmallVec or similar to avoid overhead when there are only one or two paragraphs?
166    /// Or just don't include the first paragraph, which always starts at 0?
167    pub paragraphs: Vec<ParagraphInfo>,
168}
169
170impl<'text> BidiInfo<'text> {
171    /// Split the text into paragraphs and determine the bidi embedding levels for each paragraph.
172    ///
173    /// TODO: In early steps, check for special cases that allow later steps to be skipped. like
174    /// text that is entirely LTR.  See the `nsBidi` class from Gecko for comparison.
175    ///
176    /// TODO: Support auto-RTL base direction
177    pub fn new(text: &str, default_para_level: Option<Level>) -> BidiInfo<'_> {
178        let InitialInfo {
179            original_classes,
180            paragraphs,
181            ..
182        } = InitialInfo::new(text, default_para_level);
183
184        let mut levels = Vec::<Level>::with_capacity(text.len());
185        let mut processing_classes = original_classes.clone();
186
187        for para in &paragraphs {
188            let text = &text[para.range.clone()];
189            let original_classes = &original_classes[para.range.clone()];
190            let processing_classes = &mut processing_classes[para.range.clone()];
191
192            let new_len = levels.len() + para.range.len();
193            levels.resize(new_len, para.level);
194            let levels = &mut levels[para.range.clone()];
195
196            explicit::compute(
197                text,
198                para.level,
199                original_classes,
200                levels,
201                processing_classes,
202            );
203
204            let sequences = prepare::isolating_run_sequences(para.level, original_classes, levels);
205            for sequence in &sequences {
206                implicit::resolve_weak(sequence, processing_classes);
207                implicit::resolve_neutral(sequence, levels, processing_classes);
208            }
209            implicit::resolve_levels(processing_classes, levels);
210
211            Self::assign_levels_to_removed_chars(para.level, original_classes, levels);
212        }
213
214        BidiInfo {
215            text,
216            original_classes,
217            paragraphs,
218            levels,
219        }
220    }
221
222    /// Assign levels to characters removed by rule X9.
223    ///
224    /// The levels assigned to these characters are not specified by the algorithm.  This function
225    /// assigns each one the level of the previous character, to avoid breaking level runs.
226    fn assign_levels_to_removed_chars(
227        para_level: Level,
228        classes: &[BidiClass],
229        levels: &mut [Level],
230    ) {
231        for i in 0..levels.len() {
232            if prepare::removed_by_x9(classes[i]) {
233                levels[i] = if i > 0 { levels[i - 1] } else { para_level };
234            }
235        }
236    }
237
238    /// Re-order a line based on resolved levels and return only the embedding levels, one `Level`
239    /// per *byte*.
240    pub fn reordered_levels(&self, para: &ParagraphInfo, line: Range<usize>) -> Vec<Level> {
241        let (levels, _) = self.visual_runs(para, line.clone());
242        levels
243    }
244
245    /// Re-order a line based on resolved levels and return only the embedding levels, one `Level`
246    /// per *character*.
247    pub fn reordered_levels_per_char(
248        &self,
249        para: &ParagraphInfo,
250        line: Range<usize>,
251    ) -> Vec<Level> {
252        let levels = self.reordered_levels(para, line);
253        self.text.char_indices().map(|(i, _)| levels[i]).collect()
254    }
255
256    /// Re-order a line based on resolved levels and return the line in display order.
257    pub fn reorder_line(&self, para: &ParagraphInfo, line: Range<usize>) -> Cow<'text, str> {
258        let (levels, runs) = self.visual_runs(para, line.clone());
259
260        // If all isolating run sequences are LTR, no reordering is needed
261        if runs.iter().all(|run| levels[run.start].is_ltr()) {
262            return self.text[line.clone()].into();
263        }
264
265        let mut result = String::with_capacity(line.len());
266        for run in runs {
267            if levels[run.start].is_rtl() {
268                result.extend(self.text[run].chars().rev());
269            } else {
270                result.push_str(&self.text[run]);
271            }
272        }
273        result.into()
274    }
275
276    /// Find the level runs within a line and return them in visual order.
277    ///
278    /// `line` is a range of bytes indices within `levels`.
279    ///
280    /// <https://www.unicode.org/reports/tr9/#Reordering_Resolved_Levels>
281    #[cfg_attr(feature = "cargo-clippy", allow(needless_range_loop))]
282    pub fn visual_runs(
283        &self,
284        para: &ParagraphInfo,
285        line: Range<usize>,
286    ) -> (Vec<Level>, Vec<LevelRun>) {
287        assert!(line.start <= self.levels.len());
288        assert!(line.end <= self.levels.len());
289
290        let mut levels = self.levels.clone();
291
292        // Reset some whitespace chars to paragraph level.
293        // <https://www.unicode.org/reports/tr9/#L1>
294        let line_str: &str = &self.text[line.clone()];
295        let mut reset_from: Option<usize> = Some(0);
296        let mut reset_to: Option<usize> = None;
297        for (i, c) in line_str.char_indices() {
298            match self.original_classes[i] {
299                // Ignored by X9
300                RLE | LRE | RLO | LRO | PDF | BN => {}
301                // Segment separator, Paragraph separator
302                B | S => {
303                    assert_eq!(reset_to, None);
304                    reset_to = Some(i + c.len_utf8());
305                    if reset_from == None {
306                        reset_from = Some(i);
307                    }
308                }
309                // Whitespace, isolate formatting
310                WS | FSI | LRI | RLI | PDI => {
311                    if reset_from == None {
312                        reset_from = Some(i);
313                    }
314                }
315                _ => {
316                    reset_from = None;
317                }
318            }
319            if let (Some(from), Some(to)) = (reset_from, reset_to) {
320                for j in from..to {
321                    levels[j] = para.level;
322                }
323                reset_from = None;
324                reset_to = None;
325            }
326        }
327        if let Some(from) = reset_from {
328            for j in from..line_str.len() {
329                levels[j] = para.level;
330            }
331        }
332
333        // Find consecutive level runs.
334        let mut runs = Vec::new();
335        let mut start = line.start;
336        let mut level = levels[start];
337        let mut min_level = level;
338        let mut max_level = level;
339
340        for i in (start + 1)..line.end {
341            let new_level = levels[i];
342            if new_level != level {
343                // End of the previous run, start of a new one.
344                runs.push(start..i);
345                start = i;
346                level = new_level;
347
348                min_level = min(level, min_level);
349                max_level = max(level, max_level);
350            }
351        }
352        runs.push(start..line.end);
353
354        let run_count = runs.len();
355
356        // Re-order the odd runs.
357        // <https://www.unicode.org/reports/tr9/#L2>
358
359        // Stop at the lowest *odd* level.
360        min_level = min_level.new_lowest_ge_rtl().expect("Level error");
361
362        while max_level >= min_level {
363            // Look for the start of a sequence of consecutive runs of max_level or higher.
364            let mut seq_start = 0;
365            while seq_start < run_count {
366                if self.levels[runs[seq_start].start] < max_level {
367                    seq_start += 1;
368                    continue;
369                }
370
371                // Found the start of a sequence. Now find the end.
372                let mut seq_end = seq_start + 1;
373                while seq_end < run_count {
374                    if self.levels[runs[seq_end].start] < max_level {
375                        break;
376                    }
377                    seq_end += 1;
378                }
379
380                // Reverse the runs within this sequence.
381                runs[seq_start..seq_end].reverse();
382
383                seq_start = seq_end;
384            }
385            max_level
386                .lower(1)
387                .expect("Lowering embedding level below zero");
388        }
389
390        (levels, runs)
391    }
392
393    /// If processed text has any computed RTL levels
394    ///
395    /// This information is usually used to skip re-ordering of text when no RTL level is present
396    #[inline]
397    pub fn has_rtl(&self) -> bool {
398        level::has_rtl(&self.levels)
399    }
400}
401
402impl<'text> fmt::Display for BidiInfo<'text> {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        write!(
405            f,
406            "{} paragraphs with a maximum bidirectional level of {}",
407            self.paragraphs.len(),
408            self.levels.iter().max().unwrap_or(&Level::ltr()),
409        )
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn test_initial_text_info() {
419        let text = "a1";
420        assert_eq!(
421            InitialInfo::new(text, None),
422            InitialInfo {
423                text: &text,
424                original_classes: vec![L, EN],
425                paragraphs: vec![ParagraphInfo {
426                    range: 0..2,
427                    level: LTR_LEVEL,
428                },],
429            }
430        );
431
432        let text = "غ א";
433        assert_eq!(
434            InitialInfo::new(text, None),
435            InitialInfo {
436                text: &text,
437                original_classes: vec![AL, AL, WS, R, R],
438                paragraphs: vec![ParagraphInfo {
439                    range: 0..5,
440                    level: RTL_LEVEL,
441                },],
442            }
443        );
444
445        let text = "a\u{2029}b";
446        assert_eq!(
447            InitialInfo::new(text, None),
448            InitialInfo {
449                text: &text,
450                original_classes: vec![L, B, B, B, L],
451                paragraphs: vec![
452                    ParagraphInfo {
453                        range: 0..4,
454                        level: LTR_LEVEL,
455                    },
456                    ParagraphInfo {
457                        range: 4..5,
458                        level: LTR_LEVEL,
459                    },
460                ],
461            }
462        );
463
464        let text = format!("{}א{}a", format_chars::FSI, format_chars::PDI);
465        assert_eq!(
466            InitialInfo::new(&text, None),
467            InitialInfo {
468                text: &text,
469                original_classes: vec![RLI, RLI, RLI, R, R, PDI, PDI, PDI, L],
470                paragraphs: vec![ParagraphInfo {
471                    range: 0..9,
472                    level: LTR_LEVEL,
473                },],
474            }
475        );
476    }
477
478    #[test]
479    fn test_bidi_info() {
480        let text = "abc123";
481        assert_eq!(
482            BidiInfo::new(text, Some(LTR_LEVEL)),
483            BidiInfo {
484                text: &text,
485                levels: Level::vec(&[0, 0, 0, 0, 0, 0]),
486                original_classes: vec![L, L, L, EN, EN, EN],
487                paragraphs: vec![ParagraphInfo {
488                    range: 0..6,
489                    level: LTR_LEVEL,
490                },],
491            }
492        );
493
494        let text = "abc אבג";
495        assert_eq!(
496            BidiInfo::new(text, Some(LTR_LEVEL)),
497            BidiInfo {
498                text: &text,
499                levels: Level::vec(&[0, 0, 0, 0, 1, 1, 1, 1, 1, 1]),
500                original_classes: vec![L, L, L, WS, R, R, R, R, R, R],
501                paragraphs: vec![ParagraphInfo {
502                    range: 0..10,
503                    level: LTR_LEVEL,
504                },],
505            }
506        );
507        assert_eq!(
508            BidiInfo::new(text, Some(RTL_LEVEL)),
509            BidiInfo {
510                text: &text,
511                levels: Level::vec(&[2, 2, 2, 1, 1, 1, 1, 1, 1, 1]),
512                original_classes: vec![L, L, L, WS, R, R, R, R, R, R],
513                paragraphs: vec![ParagraphInfo {
514                    range: 0..10,
515                    level: RTL_LEVEL,
516                },],
517            }
518        );
519
520        let text = "אבג abc";
521        assert_eq!(
522            BidiInfo::new(text, Some(LTR_LEVEL)),
523            BidiInfo {
524                text: &text,
525                levels: Level::vec(&[1, 1, 1, 1, 1, 1, 0, 0, 0, 0]),
526                original_classes: vec![R, R, R, R, R, R, WS, L, L, L],
527                paragraphs: vec![ParagraphInfo {
528                    range: 0..10,
529                    level: LTR_LEVEL,
530                },],
531            }
532        );
533        assert_eq!(
534            BidiInfo::new(text, None),
535            BidiInfo {
536                text: &text,
537                levels: Level::vec(&[1, 1, 1, 1, 1, 1, 1, 2, 2, 2]),
538                original_classes: vec![R, R, R, R, R, R, WS, L, L, L],
539                paragraphs: vec![ParagraphInfo {
540                    range: 0..10,
541                    level: RTL_LEVEL,
542                },],
543            }
544        );
545
546        let text = "غ2ظ א2ג";
547        assert_eq!(
548            BidiInfo::new(text, Some(LTR_LEVEL)),
549            BidiInfo {
550                text: &text,
551                levels: Level::vec(&[1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1]),
552                original_classes: vec![AL, AL, EN, AL, AL, WS, R, R, EN, R, R],
553                paragraphs: vec![ParagraphInfo {
554                    range: 0..11,
555                    level: LTR_LEVEL,
556                },],
557            }
558        );
559
560        let text = "a א.\nג";
561        assert_eq!(
562            BidiInfo::new(text, None),
563            BidiInfo {
564                text: &text,
565                original_classes: vec![L, WS, R, R, CS, B, R, R],
566                levels: Level::vec(&[0, 0, 1, 1, 0, 0, 1, 1]),
567                paragraphs: vec![
568                    ParagraphInfo {
569                        range: 0..6,
570                        level: LTR_LEVEL,
571                    },
572                    ParagraphInfo {
573                        range: 6..8,
574                        level: RTL_LEVEL,
575                    },
576                ],
577            }
578        );
579
580        // BidiTest:69635 (AL ET EN)
581        let bidi_info = BidiInfo::new("\u{060B}\u{20CF}\u{06F9}", None);
582        assert_eq!(bidi_info.original_classes, vec![AL, AL, ET, ET, ET, EN, EN]);
583    }
584
585    #[test]
586    fn test_bidi_info_has_rtl() {
587        // ASCII only
588        assert_eq!(BidiInfo::new("123", None).has_rtl(), false);
589        assert_eq!(BidiInfo::new("123", Some(LTR_LEVEL)).has_rtl(), false);
590        assert_eq!(BidiInfo::new("123", Some(RTL_LEVEL)).has_rtl(), false);
591        assert_eq!(BidiInfo::new("abc", None).has_rtl(), false);
592        assert_eq!(BidiInfo::new("abc", Some(LTR_LEVEL)).has_rtl(), false);
593        assert_eq!(BidiInfo::new("abc", Some(RTL_LEVEL)).has_rtl(), false);
594        assert_eq!(BidiInfo::new("abc 123", None).has_rtl(), false);
595        assert_eq!(BidiInfo::new("abc\n123", None).has_rtl(), false);
596
597        // With Hebrew
598        assert_eq!(BidiInfo::new("אבּג", None).has_rtl(), true);
599        assert_eq!(BidiInfo::new("אבּג", Some(LTR_LEVEL)).has_rtl(), true);
600        assert_eq!(BidiInfo::new("אבּג", Some(RTL_LEVEL)).has_rtl(), true);
601        assert_eq!(BidiInfo::new("abc אבּג", None).has_rtl(), true);
602        assert_eq!(BidiInfo::new("abc\nאבּג", None).has_rtl(), true);
603        assert_eq!(BidiInfo::new("אבּג abc", None).has_rtl(), true);
604        assert_eq!(BidiInfo::new("אבּג\nabc", None).has_rtl(), true);
605        assert_eq!(BidiInfo::new("אבּג 123", None).has_rtl(), true);
606        assert_eq!(BidiInfo::new("אבּג\n123", None).has_rtl(), true);
607    }
608
609    fn reorder_paras(text: &str) -> Vec<Cow<'_, str>> {
610        let bidi_info = BidiInfo::new(text, None);
611        bidi_info
612            .paragraphs
613            .iter()
614            .map(|para| bidi_info.reorder_line(para, para.range.clone()))
615            .collect()
616    }
617
618    #[test]
619    fn test_reorder_line() {
620        /// Bidi_Class: L L L B L L L B L L L
621        assert_eq!(
622            reorder_paras("abc\ndef\nghi"),
623            vec!["abc\n", "def\n", "ghi"]
624        );
625
626        /// Bidi_Class: L L EN B L L EN B L L EN
627        assert_eq!(
628            reorder_paras("ab1\nde2\ngh3"),
629            vec!["ab1\n", "de2\n", "gh3"]
630        );
631
632        /// Bidi_Class: L L L B AL AL AL
633        assert_eq!(reorder_paras("abc\nابج"), vec!["abc\n", "جبا"]);
634
635        /// Bidi_Class: AL AL AL B L L L
636        assert_eq!(reorder_paras("ابج\nabc"), vec!["\nجبا", "abc"]);
637
638        assert_eq!(reorder_paras("1.-2"), vec!["1.-2"]);
639        assert_eq!(reorder_paras("1-.2"), vec!["1-.2"]);
640        assert_eq!(reorder_paras("abc אבג"), vec!["abc גבא"]);
641
642        // Numbers being weak LTR characters, cannot reorder strong RTL
643        assert_eq!(reorder_paras("123 אבג"), vec!["גבא 123"]);
644
645        assert_eq!(reorder_paras("abc\u{202A}def"), vec!["abc\u{202A}def"]);
646
647        assert_eq!(
648            reorder_paras("abc\u{202A}def\u{202C}ghi"),
649            vec!["abc\u{202A}def\u{202C}ghi"]
650        );
651
652        assert_eq!(
653            reorder_paras("abc\u{2066}def\u{2069}ghi"),
654            vec!["abc\u{2066}def\u{2069}ghi"]
655        );
656
657        // Testing for RLE Character
658        assert_eq!(
659            reorder_paras("\u{202B}abc אבג\u{202C}"),
660            vec!["\u{202B}\u{202C}גבא abc"]
661        );
662
663        // Testing neutral characters
664        assert_eq!(reorder_paras("אבג? אבג"), vec!["גבא ?גבא"]);
665
666        // Testing neutral characters with special case
667        assert_eq!(reorder_paras("A אבג?"), vec!["A גבא?"]);
668
669        // Testing neutral characters with Implicit RTL Marker
670        assert_eq!(
671            reorder_paras("A אבג?\u{200F}"),
672            vec!["A \u{200F}?גבא"]
673        );
674        assert_eq!(reorder_paras("אבג abc"), vec!["abc גבא"]);
675        assert_eq!(
676            reorder_paras("abc\u{2067}.-\u{2069}ghi"),
677            vec!["abc\u{2067}-.\u{2069}ghi"]
678        );
679
680        assert_eq!(
681            reorder_paras("Hello, \u{2068}\u{202E}world\u{202C}\u{2069}!"),
682            vec!["Hello, \u{2068}\u{202E}\u{202C}dlrow\u{2069}!"]
683        );
684
685        // With mirrorable characters in RTL run
686        assert_eq!(reorder_paras("א(ב)ג."), vec![".ג)ב(א"]);
687
688        // With mirrorable characters on level boundry
689        assert_eq!(
690            reorder_paras("אב(גד[&ef].)gh"),
691            vec!["ef].)gh&[דג(בא"]
692        );
693    }
694
695    fn reordered_levels_for_paras(text: &str) -> Vec<Vec<Level>> {
696        let bidi_info = BidiInfo::new(text, None);
697        bidi_info
698            .paragraphs
699            .iter()
700            .map(|para| bidi_info.reordered_levels(para, para.range.clone()))
701            .collect()
702    }
703
704    fn reordered_levels_per_char_for_paras(text: &str) -> Vec<Vec<Level>> {
705        let bidi_info = BidiInfo::new(text, None);
706        bidi_info
707            .paragraphs
708            .iter()
709            .map(|para| bidi_info.reordered_levels_per_char(para, para.range.clone()))
710            .collect()
711    }
712
713    #[test]
714    fn test_reordered_levels() {
715        // BidiTest:946 (LRI PDI)
716        let text = "\u{2067}\u{2069}";
717        assert_eq!(
718            reordered_levels_for_paras(text),
719            vec![Level::vec(&[0, 0, 0, 0, 0, 0])]
720        );
721        assert_eq!(
722            reordered_levels_per_char_for_paras(text),
723            vec![Level::vec(&[0, 0])]
724        );
725
726        /* TODO
727        // BidiTest:69635 (AL ET EN)
728        let text = "\u{060B}\u{20CF}\u{06F9}";
729        assert_eq!(
730            reordered_levels_for_paras(text),
731            vec![Level::vec(&[1, 1, 1, 1, 1, 2, 2])]
732        );
733        assert_eq!(
734            reordered_levels_per_char_for_paras(text),
735            vec![Level::vec(&[1, 1, 2])]
736        );
737         */
738
739        /* TODO
740        // BidiTest:291284 (AN RLI PDF R)
741        assert_eq!(
742            reordered_levels_per_char_for_paras("\u{0605}\u{2067}\u{202C}\u{0590}"),
743            vec![&["2", "0", "x", "1"]]
744        );
745         */
746    }
747
748    #[test]
749    fn test_display() {
750        assert_eq!(
751            format!("{}", BidiInfo::new("", None)),
752            "0 paragraphs with a maximum bidirectional level of 0"
753        );
754
755        assert_eq!(
756            format!("{}", BidiInfo::new("abc\nאבּג", None)),
757            "2 paragraphs with a maximum bidirectional level of 1"
758        );
759    }
760}
761
762#[cfg(all(feature = "serde", test))]
763mod serde_tests {
764    use super::*;
765    use serde_test::{assert_tokens, Token};
766
767    #[test]
768    fn test_levels() {
769        let text = "abc אבג";
770        let bidi_info = BidiInfo::new(text, None);
771        let levels = bidi_info.levels;
772        assert_eq!(text.as_bytes().len(), 10);
773        assert_eq!(levels.len(), 10);
774        assert_tokens(
775            &levels,
776            &[
777                Token::Seq { len: Some(10) },
778                Token::NewtypeStruct { name: "Level" },
779                Token::U8(0),
780                Token::NewtypeStruct { name: "Level" },
781                Token::U8(0),
782                Token::NewtypeStruct { name: "Level" },
783                Token::U8(0),
784                Token::NewtypeStruct { name: "Level" },
785                Token::U8(0),
786                Token::NewtypeStruct { name: "Level" },
787                Token::U8(1),
788                Token::NewtypeStruct { name: "Level" },
789                Token::U8(1),
790                Token::NewtypeStruct { name: "Level" },
791                Token::U8(1),
792                Token::NewtypeStruct { name: "Level" },
793                Token::U8(1),
794                Token::NewtypeStruct { name: "Level" },
795                Token::U8(1),
796                Token::NewtypeStruct { name: "Level" },
797                Token::U8(1),
798                Token::SeqEnd,
799            ],
800        );
801    }
802}