use std::ops::Range;
use crate::{
bidi_level::{BidiLevel, level_runs},
break_leads::find_break_lead,
font_family_resolver::FontFamilyResolver,
paragraph::{ResolvedTextDirection, TextDirection},
script::{Script, ascii_script, script_runs},
shaping_span::ShapingSpan,
typeface::Typeface,
};
#[derive(Clone, Debug)]
pub struct TextItem<'a> {
pub range: Range<usize>,
pub level: BidiLevel,
pub paragraph_level: BidiLevel,
pub script: Script,
pub typeface: &'a Typeface,
pub span: usize,
}
#[derive(Default)]
pub struct Itemizer {
levels: Vec<(usize, BidiLevel)>,
scripts: Vec<(usize, Script)>,
fonts: Vec<(usize, usize, usize)>,
}
impl Itemizer {
pub fn itemize<'r>(
&mut self,
text: &str,
direction: TextDirection,
spans: &[ShapingSpan<'_>],
resolver: &'r FontFamilyResolver,
mut visit: impl FnMut(TextItem<'r>),
) {
let mut start = 0;
while start < text.len() {
let end = paragraph_end(text, start);
self.itemize_paragraph(text, start..end, direction, spans, resolver, &mut visit);
start = end;
}
}
fn itemize_paragraph<'r>(
&mut self,
text: &str,
paragraph: Range<usize>,
direction: TextDirection,
spans: &[ShapingSpan<'_>],
resolver: &'r FontFamilyResolver,
visit: &mut impl FnMut(TextItem<'r>),
) {
let Some(content) = text.get(paragraph.clone()) else {
return;
};
let base = match direction.resolve(content) {
ResolvedTextDirection::Ltr => BidiLevel::LTR,
ResolvedTextDirection::Rtl => BidiLevel::RTL,
};
if let [span] = spans
&& base == BidiLevel::LTR
&& content.is_ascii()
{
let script = ascii_script(content);
resolver.typeface_runs(content, span.font, |run, typeface| {
visit(TextItem {
range: paragraph.start + run.start..paragraph.start + run.end,
level: base,
paragraph_level: base,
script,
typeface: &resolver.typefaces()[typeface],
span: 0,
});
});
return;
}
self.itemize_mixed((text, content), paragraph, base, (spans, resolver), visit);
}
#[inline(never)]
fn itemize_mixed<'r>(
&mut self,
(text, content): (&str, &str),
paragraph: Range<usize>,
base: BidiLevel,
(spans, resolver): (&[ShapingSpan<'_>], &'r FontFamilyResolver),
visit: &mut impl FnMut(TextItem<'r>),
) {
self.levels.clear();
level_runs(content, base, paragraph.start, &mut self.levels);
self.scripts.clear();
script_runs(content, paragraph.start, &mut self.scripts);
self.fonts.clear();
for (index, span) in spans.iter().enumerate() {
let start = span.range.start.max(paragraph.start);
let end = span.range.end.min(paragraph.end);
if let Some(part) = text.get(start..end).filter(|part| !part.is_empty()) {
resolver.typeface_runs(part, span.font, |run, typeface| {
self.fonts.push((start + run.end, typeface, index));
});
}
}
let (mut level, mut script, mut font) = (0, 0, 0);
let mut start = paragraph.start;
while let (
Some(&(level_end, bidi)),
Some(&(script_end, writing)),
Some(&(font_end, typeface, span)),
) = (
self.levels.get(level),
self.scripts.get(script),
self.fonts.get(font),
) {
let end = level_end.min(script_end).min(font_end);
visit(TextItem {
range: start..end,
level: bidi,
paragraph_level: base,
script: writing,
typeface: &resolver.typefaces()[typeface],
span,
});
level += usize::from(level_end == end);
script += usize::from(script_end == end);
font += usize::from(font_end == end);
start = end;
}
}
}
fn paragraph_end(text: &str, start: usize) -> usize {
let bytes = text.as_bytes();
let mut at = start;
while let Some(found) = find_break_lead(bytes, at) {
at = found;
let after = match &bytes[at..] {
[b'\r', b'\n', ..] => at + 2,
[b'\n' | b'\r' | 0x1C..=0x1E, ..] => at + 1,
[0xC2, 0x85, ..] => at + 2,
[0xE2, 0x80, 0xA9, ..] => at + 3,
_ => {
at += 1;
continue;
}
};
return after;
}
text.len()
}