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, World,
15 diag::{FileError, FileResult},
16 foundations::{Bytes, Datetime},
17 layout::Abs,
18 syntax::{FileId, Source},
19 text::{Font, FontBook},
20 utils::LazyHash,
21};
22use typst_kit::fonts::{FontSearcher, Fonts};
23
24use crate::vitem::{Group, VItem, svg::SvgItem};
25use ranim_core::{
26 Extract, color,
27 components::width::Width,
28 glam,
29 primitives::vitem::VItemPrimitive,
30 traits::{Anchor, *},
31};
32
33struct TypstLruCache {
34 inner: LruCache<[u8; 20], String>,
35}
36
37impl TypstLruCache {
38 fn new(cap: NonZeroUsize) -> Self {
39 Self {
40 inner: LruCache::new(cap),
41 }
42 }
43 fn get_or_insert(&mut self, typst_str: &str) -> &String {
50 let mut sha1 = Sha1::new();
51 sha1.update(typst_str.as_bytes());
52 let sha1 = sha1.finalize();
53 self.inner
54 .get_or_insert_ref(AsRef::<[u8; 20]>::as_ref(&sha1), || {
55 let world = typst_world().lock().unwrap();
57 let world = world.with_source_str(typst_str);
58 let document = typst::compile(&world)
60 .output
61 .expect("failed to compile typst source");
62
63 let svg = typst_svg::svg_merged(&document, Abs::pt(2.0));
64 get_typst_element(&svg)
65 })
66 }
67}
68
69fn typst_lru() -> &'static Arc<Mutex<TypstLruCache>> {
70 static LRU: OnceLock<Arc<Mutex<TypstLruCache>>> = OnceLock::new();
71 LRU.get_or_init(|| {
72 Arc::new(Mutex::new(TypstLruCache::new(
73 NonZeroUsize::new(256).unwrap(),
74 )))
75 })
76}
77
78fn fonts() -> &'static Fonts {
79 static FONTS: OnceLock<Fonts> = OnceLock::new();
80 FONTS.get_or_init(|| FontSearcher::new().include_system_fonts(true).search())
81}
82
83fn typst_world() -> &'static Arc<Mutex<TypstWorld>> {
84 static WORLD: OnceLock<Arc<Mutex<TypstWorld>>> = OnceLock::new();
85 WORLD.get_or_init(|| Arc::new(Mutex::new(TypstWorld::new())))
86}
87
88pub fn typst_svg(source: &str) -> String {
90 typst_lru().lock().unwrap().get_or_insert(source).clone()
91 }
99
100struct FileEntry {
101 bytes: Bytes,
102 source: Option<Source>,
104}
105
106impl FileEntry {
107 fn source(&mut self, id: FileId) -> FileResult<Source> {
108 let source = if let Some(source) = &self.source {
110 source
111 } else {
112 let contents = std::str::from_utf8(&self.bytes).map_err(|_| FileError::InvalidUtf8)?;
113 let contents = contents.trim_start_matches('\u{feff}');
115 let source = Source::new(id, contents.into());
116 self.source.insert(source)
117 };
118 Ok(source.clone())
119 }
120}
121
122pub(crate) struct TypstWorld {
123 library: LazyHash<Library>,
124 book: LazyHash<FontBook>,
125 files: Mutex<HashMap<FileId, FileEntry>>,
126}
127
128impl TypstWorld {
129 pub(crate) fn new() -> Self {
130 let fonts = fonts();
131 Self {
132 library: LazyHash::new(Library::default()),
133 book: LazyHash::new(fonts.book.clone()),
134 files: Mutex::new(HashMap::new()),
135 }
136 }
137 pub(crate) fn with_source_str(&self, source: &str) -> TypstWorldWithSource<'_> {
138 self.with_source(Source::detached(source))
139 }
140 pub(crate) fn with_source(&self, source: Source) -> TypstWorldWithSource<'_> {
141 TypstWorldWithSource {
142 world: self,
143 source,
144 now: OnceLock::new(),
145 }
146 }
147
148 fn file<T>(&self, id: FileId, map: impl FnOnce(&mut FileEntry) -> T) -> FileResult<T> {
152 let mut files = self.files.lock().unwrap();
153 if let Some(entry) = files.get_mut(&id) {
154 return Ok(map(entry));
155 }
156 Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
175 }
176}
177
178pub(crate) struct TypstWorldWithSource<'a> {
179 world: &'a TypstWorld,
180 source: Source,
181 now: OnceLock<DateTime<Local>>,
182}
183
184impl World for TypstWorldWithSource<'_> {
185 fn library(&self) -> &LazyHash<Library> {
186 &self.world.library
187 }
188
189 fn book(&self) -> &LazyHash<FontBook> {
190 &self.world.book
191 }
192
193 fn main(&self) -> FileId {
194 self.source.id()
195 }
196
197 fn source(&self, id: FileId) -> FileResult<Source> {
198 if id == self.source.id() {
199 Ok(self.source.clone())
200 } else {
201 self.world.file(id, |entry| entry.source(id))?
202 }
203 }
204
205 fn file(&self, id: FileId) -> FileResult<Bytes> {
206 self.world.file(id, |file| file.bytes.clone())
207 }
208
209 fn font(&self, index: usize) -> Option<Font> {
210 fonts().fonts[index].get()
211 }
212
213 fn today(&self, offset: Option<i64>) -> Option<Datetime> {
214 let now = self.now.get_or_init(chrono::Local::now);
215
216 let naive = match offset {
217 None => now.naive_local(),
218 Some(o) => now.naive_utc() + chrono::Duration::hours(o),
219 };
220
221 Datetime::from_ymd(
222 naive.year(),
223 naive.month().try_into().ok()?,
224 naive.day().try_into().ok()?,
225 )
226 }
227}
228
229#[derive(Clone)]
234pub struct TypstText {
235 chars: String,
236 vitems: Group<VItem>,
237}
238
239impl TypstText {
240 fn _new(str: &str) -> Self {
241 let svg = SvgItem::new(typst_svg(str));
242 let chars = str.to_string();
243
244 let vitems = Group::<VItem>::from(svg);
245 assert_eq!(chars.len(), vitems.len());
246 Self { chars, vitems }
247 }
248 pub fn new(typst_str: &str) -> Self {
253 let svg = SvgItem::new(typst_svg(typst_str));
254 let chars = typst_str
255 .replace(" ", "")
256 .replace("\n", "")
257 .replace("\r", "")
258 .replace("\t", "");
259
260 let vitems = Group::<VItem>::from(svg);
261 assert_eq!(chars.len(), vitems.len());
262 Self { chars, vitems }
263 }
264
265 pub fn new_inline_code(code: &str) -> Self {
267 let svg = SvgItem::new(typst_svg(format!("`{code}`").as_str()));
268 let chars = code
269 .replace(" ", "")
270 .replace("\n", "")
271 .replace("\r", "")
272 .replace("\t", "");
273
274 let vitems = Group::<VItem>::from(svg);
275 assert_eq!(chars.len(), vitems.len());
276 Self { chars, vitems }
277 }
278
279 pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
281 let language = language.unwrap_or("");
282 let svg = SvgItem::new(typst_svg(format!("```{language}\n{code}```").as_str()));
284 let chars = code
285 .replace(" ", "")
286 .replace("\n", "")
287 .replace("\r", "")
288 .replace("\t", "");
289
290 let vitems = Group::<VItem>::from(svg);
291 assert_eq!(chars.len(), vitems.len());
292 Self { chars, vitems }
293 }
294}
295
296impl Alignable for TypstText {
297 fn is_aligned(&self, other: &Self) -> bool {
298 self.vitems.len() == other.vitems.len()
299 && self
300 .vitems
301 .iter()
302 .zip(&other.vitems)
303 .all(|(a, b)| a.is_aligned(b))
304 }
305 fn align_with(&mut self, other: &mut Self) {
306 let dmp = diff_match_patch_rs::DiffMatchPatch::new();
307 let diffs = dmp
308 .diff_main::<Efficient>(&self.chars, &other.chars)
309 .unwrap();
310
311 let len = self.vitems.len().max(other.vitems.len());
312 let mut vitems_self: Vec<VItem> = Vec::with_capacity(len);
313 let mut vitems_other: Vec<VItem> = Vec::with_capacity(len);
314 let mut ia = 0;
315 let mut ib = 0;
316 let mut last_neq_idx_a = 0;
317 let mut last_neq_idx_b = 0;
318 let align_and_push_diff = |vitems_self: &mut Vec<VItem>,
319 vitems_other: &mut Vec<VItem>,
320 ia,
321 ib,
322 last_neq_idx_a,
323 last_neq_idx_b| {
324 if last_neq_idx_a != ia || last_neq_idx_b != ib {
325 let mut vitems_a = self.vitems[last_neq_idx_a..ia]
326 .iter()
327 .cloned()
328 .collect::<Group<_>>();
329 let mut vitems_b = other.vitems[last_neq_idx_b..ib]
330 .iter()
331 .cloned()
332 .collect::<Group<_>>();
333 if vitems_a.is_empty() {
334 vitems_a.extend(vitems_b.iter().map(|x| {
335 x.clone().with(|x| {
336 x.shrink();
337 })
338 }));
339 }
340 if vitems_b.is_empty() {
341 vitems_b.extend(vitems_a.iter().map(|x| {
342 x.clone().with(|x| {
343 x.shrink();
344 })
345 }));
346 }
347 if last_neq_idx_a != ia && last_neq_idx_b != ib {
348 vitems_a.align_with(&mut vitems_b);
349 }
350 vitems_self.extend(vitems_a);
351 vitems_other.extend(vitems_b);
352 }
353 };
354
355 for diff in &diffs {
356 match diff.op() {
359 Ops::Equal => {
360 align_and_push_diff(
361 &mut vitems_self,
362 &mut vitems_other,
363 ia,
364 ib,
365 last_neq_idx_a,
366 last_neq_idx_b,
367 );
368 let l = diff.size();
369 vitems_self.extend(self.vitems[ia..ia + l].iter().cloned());
370 vitems_other.extend(other.vitems[ib..ib + l].iter().cloned());
371 ia += l;
372 ib += l;
373 last_neq_idx_a = ia;
374 last_neq_idx_b = ib;
375 }
376 Ops::Delete => {
377 ia += diff.size();
378 }
379 Ops::Insert => {
380 ib += diff.size();
381 }
382 }
383 }
384 align_and_push_diff(
385 &mut vitems_self,
386 &mut vitems_other,
387 ia,
388 ib,
389 last_neq_idx_a,
390 last_neq_idx_b,
391 );
392
393 assert_eq!(vitems_self.len(), vitems_other.len());
394 vitems_self
395 .iter_mut()
396 .zip(vitems_other.iter_mut())
397 .for_each(|(a, b)| {
398 if !a.is_aligned(b) {
401 a.align_with(b);
402 }
403 });
404
405 self.vitems = Group(vitems_self);
406 other.vitems = Group(vitems_other);
407 }
408}
409
410impl Interpolatable for TypstText {
411 fn lerp(&self, target: &Self, t: f64) -> Self {
412 let vitems = self
413 .vitems
414 .iter()
415 .zip(&target.vitems)
416 .map(|(a, b)| a.lerp(b, t))
417 .collect::<Group<_>>();
418 Self {
419 chars: self.chars.clone(),
420 vitems,
421 }
422 }
423}
424
425impl From<TypstText> for Group<VItem> {
426 fn from(value: TypstText) -> Self {
427 value.vitems
428 }
429}
430
431impl Extract for TypstText {
432 type Target = VItemPrimitive;
433 fn extract(&self) -> Vec<Self::Target> {
434 self.vitems.extract()
435 }
436}
437
438impl BoundingBox for TypstText {
439 fn get_bounding_box(&self) -> [glam::DVec3; 3] {
440 self.vitems.get_bounding_box()
441 }
442}
443
444impl Shift for TypstText {
445 fn shift(&mut self, shift: glam::DVec3) -> &mut Self {
446 self.vitems.shift(shift);
447 self
448 }
449}
450
451impl Rotate for TypstText {
452 fn rotate_by_anchor(&mut self, angle: f64, axis: glam::DVec3, anchor: Anchor) -> &mut Self {
453 self.vitems.rotate_by_anchor(angle, axis, anchor);
454 self
455 }
456}
457
458impl Scale for TypstText {
459 fn scale_by_anchor(&mut self, scale: glam::DVec3, anchor: Anchor) -> &mut Self {
460 self.vitems.scale_by_anchor(scale, anchor);
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
522 String::from_utf8_lossy(&removed_bg).to_string()
525}
526
527pub fn compile_typst_code(typst_code: &str) -> String {
529 let mut child = std::process::Command::new("typst")
530 .arg("compile")
531 .arg("-")
532 .arg("-")
533 .arg("-fsvg")
534 .stdin(std::process::Stdio::piped())
535 .stdout(std::process::Stdio::piped())
536 .spawn()
537 .expect("failed to spawn typst");
538
539 if let Some(mut stdin) = child.stdin.take() {
540 stdin
541 .write_all(typst_code.as_bytes())
542 .expect("failed to write to typst's stdin");
543 }
544
545 let output = child.wait_with_output().unwrap().stdout;
546 let output = String::from_utf8_lossy(&output);
547
548 get_typst_element(&output)
549}
550
551#[cfg(test)]
552mod tests {
553 use std::time::Instant;
554
555 use super::*;
556
557 #[test]
568 fn test_single_file_typst_world_foo() {
569 let start = Instant::now();
570 fonts();
571 println!("fonts search: {:?}", start.elapsed());
572
573 let start = Instant::now();
574 let world = TypstWorld::new();
575 println!("world construct: {:?}", start.elapsed());
576
577 let start = Instant::now();
578 let world = world.with_source_str("r");
579 println!("set source: {:?}", start.elapsed());
580
581 let start = Instant::now();
582 let document = typst::compile(&world)
583 .output
584 .expect("failed to compile typst source");
585 println!("document compile: {:?}", start.elapsed());
586
587 let start = Instant::now();
588 let svg = typst_svg::svg_merged(&document, Abs::pt(2.0));
589 println!("svg output: {:?}", start.elapsed());
590
591 let start = Instant::now();
592 let res = get_typst_element(&svg);
593 println!("get element: {:?}", start.elapsed());
594
595 println!("{res}");
596 }
598
599 #[test]
600 fn foo() {
601 let code_a = r#"#include <iostream>
602using namespace std;
603
604int main() {
605 cout << "Hello World!" << endl;
606}
607"#;
608 let mut code_a = TypstText::new_multiline_code(code_a, Some("cpp"));
609 let code_b = r#"fn main() {
610 println!("Hello World!");
611}"#;
612 let mut code_b = TypstText::new_multiline_code(code_b, Some("rust"));
613
614 code_a.align_with(&mut code_b);
615 }
616}