use core::{ops::Range, str::CharIndices};
use icu_properties::props::Script as IcuScript;
use parlance::{BidiLevel, Script};
use crate::{Analysis, CharInfo, ShapeOptions};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextRange {
pub byte_range: Range<usize>,
pub char_range: Range<usize>,
}
#[derive(Clone, Debug)]
pub struct Segment {
pub range: TextRange,
pub bidi_level: BidiLevel,
pub script: Script,
}
#[derive(Debug)]
pub struct Item<'a> {
pub char_end: u32,
pub options: ShapeOptions<'a>,
}
#[derive(Debug)]
pub(crate) struct Itemizer<'a> {
char_indices: CharIndices<'a>,
char_info: &'a [CharInfo],
bidi_levels: &'a [BidiLevel],
paragraph_bidi_level: BidiLevel,
current_char_offset: usize,
current_script: IcuScript,
}
impl Analysis {
pub(crate) fn itemize<'a>(&'a self, text: &'a str) -> Itemizer<'a> {
let first_real_script = self
.char_info()
.iter()
.map(|x| x.script)
.find(|&script| real_script(script))
.unwrap_or(IcuScript::Latin);
Itemizer {
char_indices: text.char_indices(),
char_info: self.char_info(),
bidi_levels: self.bidi_levels(),
paragraph_bidi_level: self.paragraph_level(),
current_char_offset: 0,
current_script: first_real_script,
}
}
}
impl Itemizer<'_> {
#[inline]
pub(crate) fn next(
&mut self,
mut split_after: impl FnMut(TextRange) -> bool,
) -> Option<Segment> {
if self.char_info.is_empty() {
debug_assert!(
self.char_indices.next().is_none() && self.bidi_levels.is_empty(),
"`char_info`, `bidi_levels`, and `char_indices` should now all be empty \
(though note `bidi_levels` may already have been empty as a special-case)"
);
return None;
}
let mut item_bidi_level = BidiLevel::new(0);
let start_byte_offset = self.char_indices.offset();
let mut item_char_len = 0;
loop {
let byte_offset = self.char_indices.offset();
let bidi_level = if self.bidi_levels.is_empty() {
self.paragraph_bidi_level
} else {
self.bidi_levels[0]
};
let mut script = self.char_info[0].script;
if !real_script(script) {
script = self.current_script;
}
if item_char_len == 0 {
item_bidi_level = bidi_level;
self.current_script = script;
}
if bidi_level != item_bidi_level || script != self.current_script {
break;
}
if item_char_len > 0
&& split_after(TextRange {
byte_range: start_byte_offset..byte_offset,
char_range: self.current_char_offset..self.current_char_offset + item_char_len,
})
{
break;
}
self.char_indices.next().expect("The passed in `text` was not of the same length as the text used to generate `Analysis`");
self.char_info = &self.char_info[1..];
if !self.bidi_levels.is_empty() {
self.bidi_levels = &self.bidi_levels[1..];
}
item_char_len += 1;
if self.char_info.is_empty() {
break;
}
}
let start_char_offset = self.current_char_offset;
self.current_char_offset += item_char_len;
Some(Segment {
range: TextRange {
byte_range: start_byte_offset..self.char_indices.offset(),
char_range: start_char_offset..self.current_char_offset,
},
script: icu_script_to_parlance_script(self.current_script),
bidi_level: item_bidi_level,
})
}
}
fn real_script(script: IcuScript) -> bool {
script != IcuScript::Common && script != IcuScript::Unknown && script != IcuScript::Inherited
}
#[inline]
fn icu_script_to_parlance_script(script: IcuScript) -> Script {
static SHORT_NAMES: icu_properties::PropertyNamesShortBorrowed<'static, IcuScript> =
icu_properties::PropertyNamesShort::new();
SHORT_NAMES
.get(script)
.and_then(|name| Script::parse(name).ok())
.unwrap_or(Script::UNKNOWN)
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use parlance::Script;
use crate::{Analysis, AnalysisOptions, Analyzer};
use super::Segment;
const LATN: Script = Script::from_bytes(*b"Latn");
const GREK: Script = Script::from_bytes(*b"Grek");
const ARAB: Script = Script::from_bytes(*b"Arab");
fn analyze(text: &str) -> Analysis {
let mut analyzer = Analyzer::new();
let mut analysis = Analysis::new();
let options = AnalysisOptions::default();
analyzer.analyze(text, &options, &mut analysis);
analysis
}
fn items(text: &str) -> Vec<Segment> {
let analysis = analyze(text);
let mut itemizer = analysis.itemize(text);
core::iter::from_fn(|| itemizer.next(|_| false)).collect()
}
#[test]
fn empty() {
assert!(items("").is_empty());
}
#[test]
fn mixed_direction() {
let text = "hello مرحبا";
let items = items(text);
assert!(items.len() >= 2);
assert_eq!(items[0].script, LATN);
assert!(items[0].bidi_level.is_ltr());
assert!(
items
.iter()
.any(|item| item.script == ARAB && item.bidi_level.is_rtl())
);
let mut cursor = 0;
for item in &items {
assert_eq!(item.range.byte_range.start, cursor);
cursor = item.range.byte_range.end;
}
assert_eq!(cursor, text.len());
}
#[test]
fn predicate() {
let text = "abcdef";
let analysis = analyze(text);
let mut itemizer = analysis.itemize(text);
let items: Vec<_> =
core::iter::from_fn(|| itemizer.next(|range| range.char_range.end == 3)).collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0].range.byte_range, 0..3);
assert_eq!(items[0].range.char_range, 0..3);
assert_eq!(items[1].range.byte_range, 3..6);
assert_eq!(items[1].range.char_range, 3..6);
}
#[test]
fn neutral_backward() {
let text = "abc αβγ";
let items = items(text);
assert_eq!(items.len(), 2);
assert_eq!(&text[items[0].range.byte_range.clone()], "abc ");
assert_eq!(items[0].script, LATN);
assert_eq!(&text[items[1].range.byte_range.clone()], "αβγ");
assert_eq!(items[1].script, GREK);
}
}