use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
use crate::foundations::{Resolve, Smart};
use crate::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use crate::model::Linebreaks;
use crate::text::{Costs, Lang, TextElem};
pub struct Preparation<'a> {
pub text: &'a str,
pub bidi: Option<BidiInfo<'a>>,
pub items: Vec<(Range, Item<'a>)>,
pub indices: Vec<usize>,
pub spans: SpanMapper,
pub hyphenate: Option<bool>,
pub costs: Costs,
pub dir: Dir,
pub lang: Option<Lang>,
pub align: FixedAlignment,
pub justify: bool,
pub hang: Abs,
pub cjk_latin_spacing: bool,
pub fallback: bool,
pub linebreaks: Smart<Linebreaks>,
pub size: Abs,
}
impl<'a> Preparation<'a> {
pub fn get(&self, offset: usize) -> &(Range, Item<'a>) {
let idx = self.indices.get(offset).copied().unwrap_or(0);
&self.items[idx]
}
pub fn slice(&self, sliced: Range) -> impl Iterator<Item = &(Range, Item<'a>)> {
let start = match sliced.start {
0 => 0,
n => self.indices.get(n).copied().unwrap_or(0),
};
self.items[start..].iter().take_while(move |(range, _)| {
range.start < sliced.end || range.end <= sliced.end
})
}
}
#[typst_macros::time]
pub fn prepare<'a>(
engine: &mut Engine,
children: &'a StyleVec,
text: &'a str,
segments: Vec<Segment<'a>>,
spans: SpanMapper,
styles: StyleChain<'a>,
) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles);
let default_level = match dir {
Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(),
};
let bidi = BidiInfo::new(text, Some(default_level));
let is_bidi = bidi
.levels
.iter()
.any(|level| level.is_ltr() != default_level.is_ltr());
let mut cursor = 0;
let mut items = Vec::with_capacity(segments.len());
for segment in segments {
let len = segment.textual_len();
let end = cursor + len;
let range = cursor..end;
match segment {
Segment::Text(_, styles) => {
shape_range(&mut items, engine, text, &bidi, range, styles);
}
Segment::Item(item) => items.push((range, item)),
}
cursor = end;
}
let mut indices = Vec::with_capacity(text.len());
for (i, (range, _)) in items.iter().enumerate() {
indices.extend(range.clone().map(|_| i));
}
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
if cjk_latin_spacing {
add_cjk_latin_spacing(&mut items);
}
Ok(Preparation {
text,
bidi: is_bidi.then_some(bidi),
items,
indices,
spans,
hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles),
dir,
lang: children.shared_get(styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles),
hang: ParElem::hanging_indent_in(styles),
cjk_latin_spacing,
fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_in(styles),
size: TextElem::size_in(styles),
})
}
fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
let mut items = items
.iter_mut()
.filter(|(_, x)| !matches!(x, Item::Tag(_)))
.peekable();
let mut prev: Option<&ShapedGlyph> = None;
while let Some((_, item)) = items.next() {
let Some(text) = item.text_mut() else {
prev = None;
continue;
};
debug_assert!(matches!(text.glyphs, std::borrow::Cow::Owned(_)));
let mut glyphs = text.glyphs.to_mut().iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
let next = glyphs.peek().map(|n| n as _).or_else(|| {
items
.peek()
.and_then(|(_, i)| i.text())
.and_then(|shaped| shaped.glyphs.first())
});
if glyph.is_cj_script() && next.is_some_and(|g| g.is_letter_or_number()) {
glyph.x_advance += Em::new(0.25);
glyph.adjustability.shrinkability.1 += Em::new(0.125);
text.width += Em::new(0.25).at(text.size);
}
if glyph.is_cj_script() && prev.is_some_and(|g| g.is_letter_or_number()) {
glyph.x_advance += Em::new(0.25);
glyph.x_offset += Em::new(0.25);
glyph.adjustability.shrinkability.0 += Em::new(0.125);
text.width += Em::new(0.25).at(text.size);
}
prev = Some(glyph);
}
}
}