use either::Either;
use typst_library::layout::{Dir, Em};
use typst_library::text::TextElem;
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
pub struct Preparation<'a> {
pub text: &'a str,
pub config: &'a Config,
pub bidi: Option<BidiInfo<'a>>,
pub items: Vec<(Range, Item<'a>)>,
pub indices: Vec<usize>,
pub spans: SpanMapper,
}
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 = (usize, &(Range, Item<'a>))> {
let start = match sliced.start {
0 => 0,
n => self.indices.get(n).copied().unwrap_or(0),
};
self.items
.iter()
.enumerate()
.skip(start)
.take_while(move |(_, (range, _))| {
range.start < sliced.end || range.end <= sliced.end
})
}
}
#[typst_macros::time]
pub fn prepare<'a>(
engine: &mut Engine,
config: &'a Config,
text: &'a str,
segments: Vec<Segment<'a>>,
spans: SpanMapper,
) -> SourceResult<Preparation<'a>> {
let default_level = match config.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));
}
if config.cjk_latin_spacing {
add_cjk_latin_spacing(&mut items);
}
Ok(Preparation {
config,
text,
bidi: is_bidi.then_some(bidi),
items,
indices,
spans,
})
}
fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
let mut iter = items
.iter_mut()
.filter(|(_, item)| !matches!(item, Item::Tag(_)))
.flat_map(|(_, item)| match item {
Item::Text(text) => Either::Left({
let shift =
text.styles.get_ref(TextElem::shift_settings).map(|shift| shift.kind);
text.glyphs.to_mut().iter_mut().map(move |g| Some((g, shift)))
}),
_ => Either::Right(std::iter::once(None)),
})
.peekable();
let mut prev: Option<(&mut ShapedGlyph, _)> = None;
while let Some(mut item) = iter.next() {
if let Some((glyph, shift)) = &mut item {
if glyph.is_cj_script()
&& let Some(Some((next_glyph, next_shift))) = iter.peek()
&& next_glyph.is_letter_or_number()
&& *shift == *next_shift
{
glyph.x_advance += Em::new(0.25);
glyph.adjustability.shrinkability.1 += Em::new(0.125);
}
if glyph.is_cj_script()
&& let Some((prev_glyph, prev_shift)) = prev
&& prev_glyph.is_letter_or_number()
&& *shift == prev_shift
{
glyph.x_advance += Em::new(0.25);
glyph.x_offset += Em::new(0.25);
glyph.adjustability.shrinkability.0 += Em::new(0.125);
}
}
prev = item;
}
}