1#[cfg(feature = "render")]
2use ranim_render::extract::{Extract, ExtractContext};
3
4use std::{
5 collections::HashMap,
6 io::Write,
7 num::NonZeroUsize,
8 sync::{Arc, Mutex, OnceLock},
9};
10
11use chrono::{DateTime, Datelike, Local};
12use diff_match_patch_rs::{Efficient, Ops};
13use lru::LruCache;
14use regex::bytes::Regex;
15use sha1::{Digest, Sha1};
16use typst::{
17 Library, LibraryExt, World,
18 diag::{FileError, FileResult},
19 foundations::{Bytes, Datetime, Duration},
20 layout::Abs,
21 syntax::{FileId, Source},
22 text::{Font, FontBook},
23 utils::LazyHash,
24};
25use typst_kit::fonts::FontStore;
26
27use crate::vitem::{VItem, svg::SvgItem};
28use ranim_core::traits::Interpolatable;
29use ranim_core::{
30 anchor::Aabb,
31 color,
32 components::width::Width,
33 glam,
34 traits::{Alignable, ApplyTransform, FillColor, Opacity, StrokeColor, StrokeWidth, With},
35};
36
37struct TypstLruCache {
38 inner: LruCache<[u8; 20], String>,
39}
40
41impl TypstLruCache {
42 fn new(cap: NonZeroUsize) -> Self {
43 Self {
44 inner: LruCache::new(cap),
45 }
46 }
47 fn get_or_insert(&mut self, typst_str: &str) -> &String {
54 let mut sha1 = Sha1::new();
55 sha1.update(typst_str.as_bytes());
56 let sha1 = sha1.finalize();
57 self.inner
58 .get_or_insert_ref(AsRef::<[u8; 20]>::as_ref(&sha1), || {
59 let world = typst_world().lock().unwrap();
61 let world = world.with_source_str(typst_str);
62 let document = typst::compile(&world)
64 .output
65 .expect("failed to compile typst source");
66
67 let svg = typst_svg::svg_merged(
68 &document,
69 &typst_svg::SvgOptions::default(),
70 Abs::pt(2.0),
71 );
72 get_typst_element(&svg)
73 })
74 }
75}
76
77fn typst_lru() -> &'static Arc<Mutex<TypstLruCache>> {
78 static LRU: OnceLock<Arc<Mutex<TypstLruCache>>> = OnceLock::new();
79 LRU.get_or_init(|| {
80 Arc::new(Mutex::new(TypstLruCache::new(
81 NonZeroUsize::new(256).unwrap(),
82 )))
83 })
84}
85
86fn fonts() -> &'static FontStore {
87 static FONTS: OnceLock<FontStore> = OnceLock::new();
88 FONTS.get_or_init(|| {
89 let mut fonts = FontStore::new();
90 fonts.extend(typst_kit::fonts::embedded());
91 fonts.extend(typst_kit::fonts::system());
92 fonts
93 })
94}
95
96fn typst_world() -> &'static Arc<Mutex<TypstWorld>> {
97 static WORLD: OnceLock<Arc<Mutex<TypstWorld>>> = OnceLock::new();
98 WORLD.get_or_init(|| Arc::new(Mutex::new(TypstWorld::new())))
99}
100
101pub fn typst_svg(source: &str) -> String {
103 typst_lru().lock().unwrap().get_or_insert(source).clone()
104 }
112
113struct FileEntry {
114 bytes: Bytes,
115 source: Option<Source>,
117}
118
119impl FileEntry {
120 fn source(&mut self, id: FileId) -> FileResult<Source> {
121 let source = if let Some(source) = &self.source {
123 source
124 } else {
125 let contents = std::str::from_utf8(&self.bytes).map_err(|_| FileError::InvalidUtf8)?;
126 let contents = contents.trim_start_matches('\u{feff}');
128 let source = Source::new(id, contents.into());
129 self.source.insert(source)
130 };
131 Ok(source.clone())
132 }
133}
134
135pub(crate) struct TypstWorld {
136 library: LazyHash<Library>,
137 book: LazyHash<FontBook>,
138 files: Mutex<HashMap<FileId, FileEntry>>,
139}
140
141impl TypstWorld {
142 pub(crate) fn new() -> Self {
143 let fonts = fonts();
144 Self {
145 library: LazyHash::new(Library::default()),
146 book: fonts.book().clone(),
147 files: Mutex::new(HashMap::new()),
148 }
149 }
150 pub(crate) fn with_source_str(&self, source: &str) -> TypstWorldWithSource<'_> {
151 self.with_source(Source::detached(source))
152 }
153 pub(crate) fn with_source(&self, source: Source) -> TypstWorldWithSource<'_> {
154 TypstWorldWithSource {
155 world: self,
156 source,
157 now: OnceLock::new(),
158 }
159 }
160
161 fn file<T>(&self, id: FileId, map: impl FnOnce(&mut FileEntry) -> T) -> FileResult<T> {
165 let mut files = self.files.lock().unwrap();
166 if let Some(entry) = files.get_mut(&id) {
167 return Ok(map(entry));
168 }
169 Err(FileError::NotFound(id.vpath().get_without_slash().into()))
188 }
189}
190
191pub(crate) struct TypstWorldWithSource<'a> {
192 world: &'a TypstWorld,
193 source: Source,
194 now: OnceLock<DateTime<Local>>,
195}
196
197impl World for TypstWorldWithSource<'_> {
198 fn library(&self) -> &LazyHash<Library> {
199 &self.world.library
200 }
201
202 fn book(&self) -> &LazyHash<FontBook> {
203 &self.world.book
204 }
205
206 fn main(&self) -> FileId {
207 self.source.id()
208 }
209
210 fn source(&self, id: FileId) -> FileResult<Source> {
211 if id == self.source.id() {
212 Ok(self.source.clone())
213 } else {
214 self.world.file(id, |entry| entry.source(id))?
215 }
216 }
217
218 fn file(&self, id: FileId) -> FileResult<Bytes> {
219 self.world.file(id, |file| file.bytes.clone())
220 }
221
222 fn font(&self, index: usize) -> Option<Font> {
223 fonts().font(index)
224 }
225
226 fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
227 let now = self.now.get_or_init(chrono::Local::now);
228
229 let naive = match offset {
230 None => now.naive_local(),
231 Some(o) => now.naive_utc() + chrono::Duration::seconds(o.seconds() as i64),
232 };
233
234 Datetime::from_ymd(
235 naive.year(),
236 naive.month().try_into().ok()?,
237 naive.day().try_into().ok()?,
238 )
239 }
240}
241
242#[derive(Clone)]
247pub struct TypstText {
248 chars: String,
249 vitems: Vec<VItem>,
250}
251
252impl TypstText {
253 fn _new(str: &str) -> Self {
254 let svg = SvgItem::new(typst_svg(str));
255 let chars = str.to_string();
256
257 let vitems = Vec::<VItem>::from(svg);
258 assert_eq!(chars.len(), vitems.len());
259 Self { chars, vitems }
260 }
261 pub fn new(typst_str: &str) -> Self {
266 let svg = SvgItem::new(typst_svg(typst_str));
267 let chars = typst_str
268 .replace(" ", "")
269 .replace("\n", "")
270 .replace("\r", "")
271 .replace("\t", "");
272
273 let vitems = Vec::<VItem>::from(svg);
274 assert_eq!(chars.len(), vitems.len());
275 Self { chars, vitems }
276 }
277
278 pub fn new_inline_code(code: &str) -> Self {
280 let svg = SvgItem::new(typst_svg(format!("`{code}`").as_str()));
281 let chars = code
282 .replace(" ", "")
283 .replace("\n", "")
284 .replace("\r", "")
285 .replace("\t", "");
286
287 let vitems = Vec::<VItem>::from(svg);
288 assert_eq!(chars.len(), vitems.len());
289 Self { chars, vitems }
290 }
291
292 pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
294 let language = language.unwrap_or("");
295 let svg = SvgItem::new(typst_svg(format!("```{language}\n{code}```").as_str()));
297 let chars = code
298 .replace(" ", "")
299 .replace("\n", "")
300 .replace("\r", "")
301 .replace("\t", "");
302
303 let vitems = Vec::<VItem>::from(svg);
304 assert_eq!(chars.len(), vitems.len());
305 Self { chars, vitems }
306 }
307
308 pub fn vitems(&self) -> &[VItem] {
310 &self.vitems
311 }
312}
313
314impl Alignable for TypstText {
315 fn is_aligned(&self, other: &Self) -> bool {
316 self.vitems.len() == other.vitems.len()
317 && self
318 .vitems
319 .iter()
320 .zip(&other.vitems)
321 .all(|(a, b)| a.is_aligned(b))
322 }
323 fn align_with(&mut self, other: &mut Self) {
324 let dmp = diff_match_patch_rs::DiffMatchPatch::new();
325 let diffs = dmp
326 .diff_main::<Efficient>(&self.chars, &other.chars)
327 .unwrap();
328
329 let len = self.vitems.len().max(other.vitems.len());
330 let mut vitems_self: Vec<VItem> = Vec::with_capacity(len);
331 let mut vitems_other: Vec<VItem> = Vec::with_capacity(len);
332 let mut ia = 0;
333 let mut ib = 0;
334 let mut last_neq_idx_a = 0;
335 let mut last_neq_idx_b = 0;
336 let align_and_push_diff = |vitems_self: &mut Vec<VItem>,
337 vitems_other: &mut Vec<VItem>,
338 ia,
339 ib,
340 last_neq_idx_a,
341 last_neq_idx_b| {
342 if last_neq_idx_a != ia || last_neq_idx_b != ib {
343 let mut vitems_a = self.vitems[last_neq_idx_a..ia].to_vec();
344 let mut vitems_b = other.vitems[last_neq_idx_b..ib].to_vec();
345 if vitems_a.is_empty() {
346 vitems_a.extend(vitems_b.iter().map(|x| {
347 x.clone().with(|x| {
348 x.shrink();
349 })
350 }));
351 }
352 if vitems_b.is_empty() {
353 vitems_b.extend(vitems_a.iter().map(|x| {
354 x.clone().with(|x| {
355 x.shrink();
356 })
357 }));
358 }
359 if last_neq_idx_a != ia && last_neq_idx_b != ib {
360 vitems_a.align_with(&mut vitems_b);
361 }
362 vitems_self.extend(vitems_a);
363 vitems_other.extend(vitems_b);
364 }
365 };
366
367 for diff in &diffs {
368 match diff.op() {
371 Ops::Equal => {
372 align_and_push_diff(
373 &mut vitems_self,
374 &mut vitems_other,
375 ia,
376 ib,
377 last_neq_idx_a,
378 last_neq_idx_b,
379 );
380 let l = diff.size();
381 vitems_self.extend(self.vitems[ia..ia + l].iter().cloned());
382 vitems_other.extend(other.vitems[ib..ib + l].iter().cloned());
383 ia += l;
384 ib += l;
385 last_neq_idx_a = ia;
386 last_neq_idx_b = ib;
387 }
388 Ops::Delete => {
389 ia += diff.size();
390 }
391 Ops::Insert => {
392 ib += diff.size();
393 }
394 }
395 }
396 align_and_push_diff(
397 &mut vitems_self,
398 &mut vitems_other,
399 ia,
400 ib,
401 last_neq_idx_a,
402 last_neq_idx_b,
403 );
404
405 assert_eq!(vitems_self.len(), vitems_other.len());
406 vitems_self
407 .iter_mut()
408 .zip(vitems_other.iter_mut())
409 .for_each(|(a, b)| {
410 if !a.is_aligned(b) {
413 a.align_with(b);
414 }
415 });
416
417 self.vitems = vitems_self;
418 other.vitems = vitems_other;
419 }
420}
421
422impl Interpolatable for TypstText {
423 fn lerp(&self, target: &Self, t: f64) -> Self {
424 let vitems = self
425 .vitems
426 .iter()
427 .zip(&target.vitems)
428 .map(|(a, b)| a.lerp(b, t))
429 .collect::<Vec<_>>();
430 Self {
431 chars: self.chars.clone(),
432 vitems,
433 }
434 }
435}
436
437impl From<TypstText> for Vec<VItem> {
438 fn from(value: TypstText) -> Self {
439 value.vitems
440 }
441}
442
443#[cfg(feature = "render")]
444impl Extract for TypstText {
445 fn extract(&self, ctx: &mut ExtractContext<'_>) {
446 for item in self.vitems() {
447 item.extract(ctx);
448 }
449 }
450}
451
452impl Aabb for TypstText {
453 fn aabb(&self) -> [glam::DVec3; 2] {
454 self.vitems.aabb()
455 }
456}
457
458impl<G: Into<glam::DAffine3>> ApplyTransform<G> for TypstText {
459 fn apply(&mut self, transform: G) -> &mut Self {
460 self.vitems.apply(transform.into());
461 self
462 }
463}
464
465impl FillColor for TypstText {
466 fn fill_color(&self) -> color::AlphaColor<color::Srgb> {
467 self.vitems[0].fill_color()
468 }
469 fn set_fill_color(&mut self, color: color::AlphaColor<color::Srgb>) -> &mut Self {
470 self.vitems.set_fill_color(color);
471 self
472 }
473 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
474 self.vitems.set_fill_opacity(opacity);
475 self
476 }
477}
478
479impl StrokeColor for TypstText {
480 fn stroke_color(&self) -> color::AlphaColor<color::Srgb> {
481 self.vitems[0].fill_color()
482 }
483 fn set_stroke_color(&mut self, color: color::AlphaColor<color::Srgb>) -> &mut Self {
484 self.vitems.set_stroke_color(color);
485 self
486 }
487 fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
488 self.vitems.set_stroke_opacity(opacity);
489 self
490 }
491}
492
493impl Opacity for TypstText {
494 fn set_opacity(&mut self, opacity: f32) -> &mut Self {
495 self.vitems.set_fill_opacity(opacity);
496 self.vitems.set_stroke_opacity(opacity);
497 self
498 }
499}
500
501impl StrokeWidth for TypstText {
502 fn stroke_width(&self) -> f32 {
503 self.vitems.stroke_width()
504 }
505 fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
506 self.vitems.iter_mut().for_each(|vitem| {
507 vitem.apply_stroke_func(&f);
508 });
509 self
510 }
511 fn set_stroke_width(&mut self, width: f32) -> &mut Self {
512 self.vitems.set_stroke_width(width);
513 self
514 }
515}
516
517pub fn get_typst_element(svg: &str) -> String {
519 let re = Regex::new(r"<path[^>]*(?:>.*?<\/path>|\/>)").unwrap();
520 let removed_bg = re.replace(svg.as_bytes(), b"");
521 let re = Regex::new(r#"\s+(?:viewBox|width|height)="[^"]*""#).unwrap();
522 let removed_size = re.replace_all(&removed_bg, b"");
523
524 String::from_utf8_lossy(&removed_size).to_string()
527}
528
529pub fn compile_typst_code(typst_code: &str) -> String {
531 let mut child = std::process::Command::new("typst")
532 .arg("compile")
533 .arg("-")
534 .arg("-")
535 .arg("-fsvg")
536 .stdin(std::process::Stdio::piped())
537 .stdout(std::process::Stdio::piped())
538 .spawn()
539 .expect("failed to spawn typst");
540
541 if let Some(mut stdin) = child.stdin.take() {
542 stdin
543 .write_all(typst_code.as_bytes())
544 .expect("failed to write to typst's stdin");
545 }
546
547 let output = child.wait_with_output().unwrap().stdout;
548 let output = String::from_utf8_lossy(&output);
549
550 output.to_string()
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn typst_element_strips_background_paths_and_canvas_size() {
559 let svg = r#"<svg viewBox="0 0 5 5" width="5pt" height="5pt"><path d="M0 0"/><g/></svg>"#;
560 assert_eq!(get_typst_element(svg), "<svg><g/></svg>");
561 }
562}