1use std::ops::Range;
2
3use crate::{
4 bidi_level::{BidiLevel, level_runs},
5 break_leads::find_break_lead,
6 font_family_resolver::FontFamilyResolver,
7 paragraph::{ResolvedTextDirection, TextDirection},
8 script::{Script, ascii_script, script_runs},
9 shaping_span::ShapingSpan,
10 typeface::Typeface,
11};
12
13#[derive(Clone, Debug)]
16pub struct TextItem<'a> {
17 pub range: Range<usize>,
19 pub level: BidiLevel,
21 pub paragraph_level: BidiLevel,
23 pub script: Script,
25 pub typeface: &'a Typeface,
27 pub span: usize,
29}
30
31#[derive(Default)]
36pub struct Itemizer {
37 levels: Vec<(usize, BidiLevel)>,
38 scripts: Vec<(usize, Script)>,
39 fonts: Vec<(usize, usize, usize)>,
41}
42
43impl Itemizer {
44 pub fn itemize<'r>(
47 &mut self,
48 text: &str,
49 direction: TextDirection,
50 spans: &[ShapingSpan<'_>],
51 resolver: &'r FontFamilyResolver,
52 mut visit: impl FnMut(TextItem<'r>),
53 ) {
54 let mut start = 0;
55 while start < text.len() {
56 let end = paragraph_end(text, start);
57 self.itemize_paragraph(text, start..end, direction, spans, resolver, &mut visit);
58 start = end;
59 }
60 }
61
62 fn itemize_paragraph<'r>(
63 &mut self,
64 text: &str,
65 paragraph: Range<usize>,
66 direction: TextDirection,
67 spans: &[ShapingSpan<'_>],
68 resolver: &'r FontFamilyResolver,
69 visit: &mut impl FnMut(TextItem<'r>),
70 ) {
71 let Some(content) = text.get(paragraph.clone()) else {
72 return;
73 };
74 let base = match direction.resolve(content) {
75 ResolvedTextDirection::Ltr => BidiLevel::LTR,
76 ResolvedTextDirection::Rtl => BidiLevel::RTL,
77 };
78 if let [span] = spans
81 && base == BidiLevel::LTR
82 && content.is_ascii()
83 {
84 let script = ascii_script(content);
85 resolver.typeface_runs(content, span.font, |run, typeface| {
86 visit(TextItem {
87 range: paragraph.start + run.start..paragraph.start + run.end,
88 level: base,
89 paragraph_level: base,
90 script,
91 typeface: &resolver.typefaces()[typeface],
92 span: 0,
93 });
94 });
95 return;
96 }
97 self.itemize_mixed((text, content), paragraph, base, (spans, resolver), visit);
98 }
99
100 #[inline(never)]
105 fn itemize_mixed<'r>(
106 &mut self,
107 (text, content): (&str, &str),
108 paragraph: Range<usize>,
109 base: BidiLevel,
110 (spans, resolver): (&[ShapingSpan<'_>], &'r FontFamilyResolver),
111 visit: &mut impl FnMut(TextItem<'r>),
112 ) {
113 self.levels.clear();
114 level_runs(content, base, paragraph.start, &mut self.levels);
115 self.scripts.clear();
116 script_runs(content, paragraph.start, &mut self.scripts);
117 self.fonts.clear();
118 for (index, span) in spans.iter().enumerate() {
119 let start = span.range.start.max(paragraph.start);
120 let end = span.range.end.min(paragraph.end);
121 if let Some(part) = text.get(start..end).filter(|part| !part.is_empty()) {
122 resolver.typeface_runs(part, span.font, |run, typeface| {
123 self.fonts.push((start + run.end, typeface, index));
124 });
125 }
126 }
127 let (mut level, mut script, mut font) = (0, 0, 0);
128 let mut start = paragraph.start;
129 while let (
130 Some(&(level_end, bidi)),
131 Some(&(script_end, writing)),
132 Some(&(font_end, typeface, span)),
133 ) = (
134 self.levels.get(level),
135 self.scripts.get(script),
136 self.fonts.get(font),
137 ) {
138 let end = level_end.min(script_end).min(font_end);
139 visit(TextItem {
140 range: start..end,
141 level: bidi,
142 paragraph_level: base,
143 script: writing,
144 typeface: &resolver.typefaces()[typeface],
145 span,
146 });
147 level += usize::from(level_end == end);
148 script += usize::from(script_end == end);
149 font += usize::from(font_end == end);
150 start = end;
151 }
152 }
153}
154
155fn paragraph_end(text: &str, start: usize) -> usize {
161 let bytes = text.as_bytes();
162 let mut at = start;
163 while let Some(found) = find_break_lead(bytes, at) {
164 at = found;
165 let after = match &bytes[at..] {
166 [b'\r', b'\n', ..] => at + 2,
167 [b'\n' | b'\r' | 0x1C..=0x1E, ..] => at + 1,
168 [0xC2, 0x85, ..] => at + 2,
169 [0xE2, 0x80, 0xA9, ..] => at + 3,
170 _ => {
171 at += 1;
172 continue;
173 }
174 };
175 return after;
176 }
177 text.len()
178}