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