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::{
34 Alignable, FillColor, Opacity, RotateTransform, ScaleTransform, ShiftTransform,
35 StrokeColor, StrokeWidth, With,
36 },
37};
38
39struct TypstLruCache {
40 inner: LruCache<[u8; 20], String>,
41}
42
43impl TypstLruCache {
44 fn new(cap: NonZeroUsize) -> Self {
45 Self {
46 inner: LruCache::new(cap),
47 }
48 }
49 fn get_or_insert(&mut self, typst_str: &str) -> &String {
56 let mut sha1 = Sha1::new();
57 sha1.update(typst_str.as_bytes());
58 let sha1 = sha1.finalize();
59 self.inner
60 .get_or_insert_ref(AsRef::<[u8; 20]>::as_ref(&sha1), || {
61 let world = typst_world().lock().unwrap();
63 let world = world.with_source_str(typst_str);
64 let document = typst::compile(&world)
66 .output
67 .expect("failed to compile typst source");
68
69 let svg = typst_svg::svg_merged(
70 &document,
71 &typst_svg::SvgOptions::default(),
72 Abs::pt(2.0),
73 );
74 get_typst_element(&svg)
75 })
76 }
77}
78
79fn typst_lru() -> &'static Arc<Mutex<TypstLruCache>> {
80 static LRU: OnceLock<Arc<Mutex<TypstLruCache>>> = OnceLock::new();
81 LRU.get_or_init(|| {
82 Arc::new(Mutex::new(TypstLruCache::new(
83 NonZeroUsize::new(256).unwrap(),
84 )))
85 })
86}
87
88fn fonts() -> &'static FontStore {
89 static FONTS: OnceLock<FontStore> = OnceLock::new();
90 FONTS.get_or_init(|| {
91 let mut fonts = FontStore::new();
92 fonts.extend(typst_kit::fonts::embedded());
93 fonts.extend(typst_kit::fonts::system());
94 fonts
95 })
96}
97
98fn typst_world() -> &'static Arc<Mutex<TypstWorld>> {
99 static WORLD: OnceLock<Arc<Mutex<TypstWorld>>> = OnceLock::new();
100 WORLD.get_or_init(|| Arc::new(Mutex::new(TypstWorld::new())))
101}
102
103pub fn typst_svg(source: &str) -> String {
105 typst_lru().lock().unwrap().get_or_insert(source).clone()
106 }
114
115struct FileEntry {
116 bytes: Bytes,
117 source: Option<Source>,
119}
120
121impl FileEntry {
122 fn source(&mut self, id: FileId) -> FileResult<Source> {
123 let source = if let Some(source) = &self.source {
125 source
126 } else {
127 let contents = std::str::from_utf8(&self.bytes).map_err(|_| FileError::InvalidUtf8)?;
128 let contents = contents.trim_start_matches('\u{feff}');
130 let source = Source::new(id, contents.into());
131 self.source.insert(source)
132 };
133 Ok(source.clone())
134 }
135}
136
137pub(crate) struct TypstWorld {
138 library: LazyHash<Library>,
139 book: LazyHash<FontBook>,
140 files: Mutex<HashMap<FileId, FileEntry>>,
141}
142
143impl TypstWorld {
144 pub(crate) fn new() -> Self {
145 let fonts = fonts();
146 Self {
147 library: LazyHash::new(Library::default()),
148 book: fonts.book().clone(),
149 files: Mutex::new(HashMap::new()),
150 }
151 }
152 pub(crate) fn with_source_str(&self, source: &str) -> TypstWorldWithSource<'_> {
153 self.with_source(Source::detached(source))
154 }
155 pub(crate) fn with_source(&self, source: Source) -> TypstWorldWithSource<'_> {
156 TypstWorldWithSource {
157 world: self,
158 source,
159 now: OnceLock::new(),
160 }
161 }
162
163 fn file<T>(&self, id: FileId, map: impl FnOnce(&mut FileEntry) -> T) -> FileResult<T> {
167 let mut files = self.files.lock().unwrap();
168 if let Some(entry) = files.get_mut(&id) {
169 return Ok(map(entry));
170 }
171 Err(FileError::NotFound(id.vpath().get_without_slash().into()))
190 }
191}
192
193pub(crate) struct TypstWorldWithSource<'a> {
194 world: &'a TypstWorld,
195 source: Source,
196 now: OnceLock<DateTime<Local>>,
197}
198
199impl World for TypstWorldWithSource<'_> {
200 fn library(&self) -> &LazyHash<Library> {
201 &self.world.library
202 }
203
204 fn book(&self) -> &LazyHash<FontBook> {
205 &self.world.book
206 }
207
208 fn main(&self) -> FileId {
209 self.source.id()
210 }
211
212 fn source(&self, id: FileId) -> FileResult<Source> {
213 if id == self.source.id() {
214 Ok(self.source.clone())
215 } else {
216 self.world.file(id, |entry| entry.source(id))?
217 }
218 }
219
220 fn file(&self, id: FileId) -> FileResult<Bytes> {
221 self.world.file(id, |file| file.bytes.clone())
222 }
223
224 fn font(&self, index: usize) -> Option<Font> {
225 fonts().font(index)
226 }
227
228 fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
229 let now = self.now.get_or_init(chrono::Local::now);
230
231 let naive = match offset {
232 None => now.naive_local(),
233 Some(o) => now.naive_utc() + chrono::Duration::seconds(o.seconds() as i64),
234 };
235
236 Datetime::from_ymd(
237 naive.year(),
238 naive.month().try_into().ok()?,
239 naive.day().try_into().ok()?,
240 )
241 }
242}
243
244#[derive(Clone)]
249pub struct TypstText {
250 chars: String,
251 vitems: Vec<VItem>,
252}
253
254impl TypstText {
255 fn _new(str: &str) -> Self {
256 let svg = SvgItem::new(typst_svg(str));
257 let chars = str.to_string();
258
259 let vitems = Vec::<VItem>::from(svg);
260 assert_eq!(chars.len(), vitems.len());
261 Self { chars, vitems }
262 }
263 pub fn new(typst_str: &str) -> Self {
268 let svg = SvgItem::new(typst_svg(typst_str));
269 let chars = typst_str
270 .replace(" ", "")
271 .replace("\n", "")
272 .replace("\r", "")
273 .replace("\t", "");
274
275 let vitems = Vec::<VItem>::from(svg);
276 assert_eq!(chars.len(), vitems.len());
277 Self { chars, vitems }
278 }
279
280 pub fn new_inline_code(code: &str) -> Self {
282 let svg = SvgItem::new(typst_svg(format!("`{code}`").as_str()));
283 let chars = code
284 .replace(" ", "")
285 .replace("\n", "")
286 .replace("\r", "")
287 .replace("\t", "");
288
289 let vitems = Vec::<VItem>::from(svg);
290 assert_eq!(chars.len(), vitems.len());
291 Self { chars, vitems }
292 }
293
294 pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
296 let language = language.unwrap_or("");
297 let svg = SvgItem::new(typst_svg(format!("```{language}\n{code}```").as_str()));
299 let chars = code
300 .replace(" ", "")
301 .replace("\n", "")
302 .replace("\r", "")
303 .replace("\t", "");
304
305 let vitems = Vec::<VItem>::from(svg);
306 assert_eq!(chars.len(), vitems.len());
307 Self { chars, vitems }
308 }
309}
310
311impl Alignable for TypstText {
312 fn is_aligned(&self, other: &Self) -> bool {
313 self.vitems.len() == other.vitems.len()
314 && self
315 .vitems
316 .iter()
317 .zip(&other.vitems)
318 .all(|(a, b)| a.is_aligned(b))
319 }
320 fn align_with(&mut self, other: &mut Self) {
321 let dmp = diff_match_patch_rs::DiffMatchPatch::new();
322 let diffs = dmp
323 .diff_main::<Efficient>(&self.chars, &other.chars)
324 .unwrap();
325
326 let len = self.vitems.len().max(other.vitems.len());
327 let mut vitems_self: Vec<VItem> = Vec::with_capacity(len);
328 let mut vitems_other: Vec<VItem> = Vec::with_capacity(len);
329 let mut ia = 0;
330 let mut ib = 0;
331 let mut last_neq_idx_a = 0;
332 let mut last_neq_idx_b = 0;
333 let align_and_push_diff = |vitems_self: &mut Vec<VItem>,
334 vitems_other: &mut Vec<VItem>,
335 ia,
336 ib,
337 last_neq_idx_a,
338 last_neq_idx_b| {
339 if last_neq_idx_a != ia || last_neq_idx_b != ib {
340 let mut vitems_a = self.vitems[last_neq_idx_a..ia].to_vec();
341 let mut vitems_b = other.vitems[last_neq_idx_b..ib].to_vec();
342 if vitems_a.is_empty() {
343 vitems_a.extend(vitems_b.iter().map(|x| {
344 x.clone().with(|x| {
345 x.shrink();
346 })
347 }));
348 }
349 if vitems_b.is_empty() {
350 vitems_b.extend(vitems_a.iter().map(|x| {
351 x.clone().with(|x| {
352 x.shrink();
353 })
354 }));
355 }
356 if last_neq_idx_a != ia && last_neq_idx_b != ib {
357 vitems_a.align_with(&mut vitems_b);
358 }
359 vitems_self.extend(vitems_a);
360 vitems_other.extend(vitems_b);
361 }
362 };
363
364 for diff in &diffs {
365 match diff.op() {
368 Ops::Equal => {
369 align_and_push_diff(
370 &mut vitems_self,
371 &mut vitems_other,
372 ia,
373 ib,
374 last_neq_idx_a,
375 last_neq_idx_b,
376 );
377 let l = diff.size();
378 vitems_self.extend(self.vitems[ia..ia + l].iter().cloned());
379 vitems_other.extend(other.vitems[ib..ib + l].iter().cloned());
380 ia += l;
381 ib += l;
382 last_neq_idx_a = ia;
383 last_neq_idx_b = ib;
384 }
385 Ops::Delete => {
386 ia += diff.size();
387 }
388 Ops::Insert => {
389 ib += diff.size();
390 }
391 }
392 }
393 align_and_push_diff(
394 &mut vitems_self,
395 &mut vitems_other,
396 ia,
397 ib,
398 last_neq_idx_a,
399 last_neq_idx_b,
400 );
401
402 assert_eq!(vitems_self.len(), vitems_other.len());
403 vitems_self
404 .iter_mut()
405 .zip(vitems_other.iter_mut())
406 .for_each(|(a, b)| {
407 if !a.is_aligned(b) {
410 a.align_with(b);
411 }
412 });
413
414 self.vitems = vitems_self;
415 other.vitems = vitems_other;
416 }
417}
418
419impl Interpolatable for TypstText {
420 fn lerp(&self, target: &Self, t: f64) -> Self {
421 let vitems = self
422 .vitems
423 .iter()
424 .zip(&target.vitems)
425 .map(|(a, b)| a.lerp(b, t))
426 .collect::<Vec<_>>();
427 Self {
428 chars: self.chars.clone(),
429 vitems,
430 }
431 }
432}
433
434impl From<TypstText> for Vec<VItem> {
435 fn from(value: TypstText) -> Self {
436 value.vitems
437 }
438}
439
440impl Extract for TypstText {
441 type Target = CoreItem;
442 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
443 self.vitems.extract_into(buf);
444 }
445}
446
447impl Aabb for TypstText {
448 fn aabb(&self) -> [glam::DVec3; 2] {
449 self.vitems.aabb()
450 }
451}
452
453impl ShiftTransform for TypstText {
454 fn shift(&mut self, shift: glam::DVec3) -> &mut Self {
455 self.vitems.shift(shift);
456 self
457 }
458}
459
460impl RotateTransform for TypstText {
461 fn rotate_on_axis(&mut self, axis: glam::DVec3, angle: f64) -> &mut Self {
462 self.vitems.rotate_on_axis(axis, angle);
463 self
464 }
465}
466
467impl ScaleTransform for TypstText {
468 fn scale(&mut self, scale: glam::DVec3) -> &mut Self {
469 self.vitems.scale(scale);
470 self
471 }
472}
473
474impl FillColor for TypstText {
475 fn fill_color(&self) -> color::AlphaColor<color::Srgb> {
476 self.vitems[0].fill_color()
477 }
478 fn set_fill_color(&mut self, color: color::AlphaColor<color::Srgb>) -> &mut Self {
479 self.vitems.set_fill_color(color);
480 self
481 }
482 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
483 self.vitems.set_fill_opacity(opacity);
484 self
485 }
486}
487
488impl StrokeColor for TypstText {
489 fn stroke_color(&self) -> color::AlphaColor<color::Srgb> {
490 self.vitems[0].fill_color()
491 }
492 fn set_stroke_color(&mut self, color: color::AlphaColor<color::Srgb>) -> &mut Self {
493 self.vitems.set_stroke_color(color);
494 self
495 }
496 fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
497 self.vitems.set_stroke_opacity(opacity);
498 self
499 }
500}
501
502impl Opacity for TypstText {
503 fn set_opacity(&mut self, opacity: f32) -> &mut Self {
504 self.vitems.set_fill_opacity(opacity);
505 self.vitems.set_stroke_opacity(opacity);
506 self
507 }
508}
509
510impl StrokeWidth for TypstText {
511 fn stroke_width(&self) -> f32 {
512 self.vitems.stroke_width()
513 }
514 fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
515 self.vitems.iter_mut().for_each(|vitem| {
516 vitem.apply_stroke_func(&f);
517 });
518 self
519 }
520 fn set_stroke_width(&mut self, width: f32) -> &mut Self {
521 self.vitems.set_stroke_width(width);
522 self
523 }
524}
525
526pub fn get_typst_element(svg: &str) -> String {
528 let re = Regex::new(r"<path[^>]*(?:>.*?<\/path>|\/>)").unwrap();
529 let removed_bg = re.replace(svg.as_bytes(), b"");
530 let re = Regex::new(r#"\s+(?:viewBox|width|height)="[^"]*""#).unwrap();
531 let removed_size = re.replace_all(&removed_bg, b"");
532
533 String::from_utf8_lossy(&removed_size).to_string()
536}
537
538pub fn compile_typst_code(typst_code: &str) -> String {
540 let mut child = std::process::Command::new("typst")
541 .arg("compile")
542 .arg("-")
543 .arg("-")
544 .arg("-fsvg")
545 .stdin(std::process::Stdio::piped())
546 .stdout(std::process::Stdio::piped())
547 .spawn()
548 .expect("failed to spawn typst");
549
550 if let Some(mut stdin) = child.stdin.take() {
551 stdin
552 .write_all(typst_code.as_bytes())
553 .expect("failed to write to typst's stdin");
554 }
555
556 let output = child.wait_with_output().unwrap().stdout;
557 let output = String::from_utf8_lossy(&output);
558
559 output.to_string()
560}
561
562#[cfg(test)]
563mod tests {
564 use std::time::Instant;
565
566 use super::*;
567
568 #[test]
579 fn test_single_file_typst_world_foo() {
580 let start = Instant::now();
581 fonts();
582 println!("fonts search: {:?}", start.elapsed());
583
584 let start = Instant::now();
585 let world = TypstWorld::new();
586 println!("world construct: {:?}", start.elapsed());
587
588 let start = Instant::now();
589 let world = world.with_source_str("r");
590 println!("set source: {:?}", start.elapsed());
591
592 let start = Instant::now();
593 let document = typst::compile(&world)
594 .output
595 .expect("failed to compile typst source");
596 println!("document compile: {:?}", start.elapsed());
597
598 let start = Instant::now();
599 let svg = typst_svg::svg_merged(&document, &typst_svg::SvgOptions::default(), Abs::pt(2.0));
600 println!("{svg}");
601 println!("svg output: {:?}", start.elapsed());
602
603 let start = Instant::now();
604 let res = get_typst_element(&svg);
605 println!("get element: {:?}", start.elapsed());
606
607 println!("{res}");
608 }
610
611 #[test]
632 fn foo_page() {
633 let text = r#"Ra"#;
634 let res = compile_typst_code(text);
635 println!("{res}");
636
637 let res = typst_svg(text);
638 println!("{res}");
639 }
640
641 #[test]
642 fn foo() {
643 let code_a = r#"#include <iostream>
644using namespace std;
645
646int main() {
647 cout << "Hello World!" << endl;
648}
649"#;
650 let mut code_a = TypstText::new_multiline_code(code_a, Some("cpp"));
651 let code_b = r#"fn main() {
652 println!("Hello World!");
653}"#;
654 let mut code_b = TypstText::new_multiline_code(code_b, Some("rust"));
655
656 code_a.align_with(&mut code_b);
657 }
658}