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},
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::{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(&document, Abs::pt(2.0));
70 get_typst_element(&svg)
71 })
72 }
73}
74
75fn typst_lru() -> &'static Arc<Mutex<TypstLruCache>> {
76 static LRU: OnceLock<Arc<Mutex<TypstLruCache>>> = OnceLock::new();
77 LRU.get_or_init(|| {
78 Arc::new(Mutex::new(TypstLruCache::new(
79 NonZeroUsize::new(256).unwrap(),
80 )))
81 })
82}
83
84fn fonts() -> &'static Fonts {
85 static FONTS: OnceLock<Fonts> = OnceLock::new();
86 FONTS.get_or_init(|| FontSearcher::new().include_system_fonts(true).search())
87}
88
89fn typst_world() -> &'static Arc<Mutex<TypstWorld>> {
90 static WORLD: OnceLock<Arc<Mutex<TypstWorld>>> = OnceLock::new();
91 WORLD.get_or_init(|| Arc::new(Mutex::new(TypstWorld::new())))
92}
93
94pub fn typst_svg(source: &str) -> String {
96 typst_lru().lock().unwrap().get_or_insert(source).clone()
97 }
105
106struct FileEntry {
107 bytes: Bytes,
108 source: Option<Source>,
110}
111
112impl FileEntry {
113 fn source(&mut self, id: FileId) -> FileResult<Source> {
114 let source = if let Some(source) = &self.source {
116 source
117 } else {
118 let contents = std::str::from_utf8(&self.bytes).map_err(|_| FileError::InvalidUtf8)?;
119 let contents = contents.trim_start_matches('\u{feff}');
121 let source = Source::new(id, contents.into());
122 self.source.insert(source)
123 };
124 Ok(source.clone())
125 }
126}
127
128pub(crate) struct TypstWorld {
129 library: LazyHash<Library>,
130 book: LazyHash<FontBook>,
131 files: Mutex<HashMap<FileId, FileEntry>>,
132}
133
134impl TypstWorld {
135 pub(crate) fn new() -> Self {
136 let fonts = fonts();
137 Self {
138 library: LazyHash::new(Library::default()),
139 book: LazyHash::new(fonts.book.clone()),
140 files: Mutex::new(HashMap::new()),
141 }
142 }
143 pub(crate) fn with_source_str(&self, source: &str) -> TypstWorldWithSource<'_> {
144 self.with_source(Source::detached(source))
145 }
146 pub(crate) fn with_source(&self, source: Source) -> TypstWorldWithSource<'_> {
147 TypstWorldWithSource {
148 world: self,
149 source,
150 now: OnceLock::new(),
151 }
152 }
153
154 fn file<T>(&self, id: FileId, map: impl FnOnce(&mut FileEntry) -> T) -> FileResult<T> {
158 let mut files = self.files.lock().unwrap();
159 if let Some(entry) = files.get_mut(&id) {
160 return Ok(map(entry));
161 }
162 Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
181 }
182}
183
184pub(crate) struct TypstWorldWithSource<'a> {
185 world: &'a TypstWorld,
186 source: Source,
187 now: OnceLock<DateTime<Local>>,
188}
189
190impl World for TypstWorldWithSource<'_> {
191 fn library(&self) -> &LazyHash<Library> {
192 &self.world.library
193 }
194
195 fn book(&self) -> &LazyHash<FontBook> {
196 &self.world.book
197 }
198
199 fn main(&self) -> FileId {
200 self.source.id()
201 }
202
203 fn source(&self, id: FileId) -> FileResult<Source> {
204 if id == self.source.id() {
205 Ok(self.source.clone())
206 } else {
207 self.world.file(id, |entry| entry.source(id))?
208 }
209 }
210
211 fn file(&self, id: FileId) -> FileResult<Bytes> {
212 self.world.file(id, |file| file.bytes.clone())
213 }
214
215 fn font(&self, index: usize) -> Option<Font> {
216 fonts().fonts[index].get()
217 }
218
219 fn today(&self, offset: Option<i64>) -> Option<Datetime> {
220 let now = self.now.get_or_init(chrono::Local::now);
221
222 let naive = match offset {
223 None => now.naive_local(),
224 Some(o) => now.naive_utc() + chrono::Duration::hours(o),
225 };
226
227 Datetime::from_ymd(
228 naive.year(),
229 naive.month().try_into().ok()?,
230 naive.day().try_into().ok()?,
231 )
232 }
233}
234
235#[derive(Clone)]
240pub struct TypstText {
241 chars: String,
242 vitems: Vec<VItem>,
243}
244
245impl TypstText {
246 fn _new(str: &str) -> Self {
247 let svg = SvgItem::new(typst_svg(str));
248 let chars = str.to_string();
249
250 let vitems = Vec::<VItem>::from(svg);
251 assert_eq!(chars.len(), vitems.len());
252 Self { chars, vitems }
253 }
254 pub fn new(typst_str: &str) -> Self {
259 let svg = SvgItem::new(typst_svg(typst_str));
260 let chars = typst_str
261 .replace(" ", "")
262 .replace("\n", "")
263 .replace("\r", "")
264 .replace("\t", "");
265
266 let vitems = Vec::<VItem>::from(svg);
267 assert_eq!(chars.len(), vitems.len());
268 Self { chars, vitems }
269 }
270
271 pub fn new_inline_code(code: &str) -> Self {
273 let svg = SvgItem::new(typst_svg(format!("`{code}`").as_str()));
274 let chars = code
275 .replace(" ", "")
276 .replace("\n", "")
277 .replace("\r", "")
278 .replace("\t", "");
279
280 let vitems = Vec::<VItem>::from(svg);
281 assert_eq!(chars.len(), vitems.len());
282 Self { chars, vitems }
283 }
284
285 pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
287 let language = language.unwrap_or("");
288 let svg = SvgItem::new(typst_svg(format!("```{language}\n{code}```").as_str()));
290 let chars = code
291 .replace(" ", "")
292 .replace("\n", "")
293 .replace("\r", "")
294 .replace("\t", "");
295
296 let vitems = Vec::<VItem>::from(svg);
297 assert_eq!(chars.len(), vitems.len());
298 Self { chars, vitems }
299 }
300}
301
302impl Alignable for TypstText {
303 fn is_aligned(&self, other: &Self) -> bool {
304 self.vitems.len() == other.vitems.len()
305 && self
306 .vitems
307 .iter()
308 .zip(&other.vitems)
309 .all(|(a, b)| a.is_aligned(b))
310 }
311 fn align_with(&mut self, other: &mut Self) {
312 let dmp = diff_match_patch_rs::DiffMatchPatch::new();
313 let diffs = dmp
314 .diff_main::<Efficient>(&self.chars, &other.chars)
315 .unwrap();
316
317 let len = self.vitems.len().max(other.vitems.len());
318 let mut vitems_self: Vec<VItem> = Vec::with_capacity(len);
319 let mut vitems_other: Vec<VItem> = Vec::with_capacity(len);
320 let mut ia = 0;
321 let mut ib = 0;
322 let mut last_neq_idx_a = 0;
323 let mut last_neq_idx_b = 0;
324 let align_and_push_diff = |vitems_self: &mut Vec<VItem>,
325 vitems_other: &mut Vec<VItem>,
326 ia,
327 ib,
328 last_neq_idx_a,
329 last_neq_idx_b| {
330 if last_neq_idx_a != ia || last_neq_idx_b != ib {
331 let mut vitems_a = self.vitems[last_neq_idx_a..ia].to_vec();
332 let mut vitems_b = other.vitems[last_neq_idx_b..ib].to_vec();
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 = vitems_self;
406 other.vitems = 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::<Vec<_>>();
418 Self {
419 chars: self.chars.clone(),
420 vitems,
421 }
422 }
423}
424
425impl From<TypstText> for Vec<VItem> {
426 fn from(value: TypstText) -> Self {
427 value.vitems
428 }
429}
430
431impl Extract for TypstText {
432 type Target = CoreItem;
433 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
434 self.vitems.extract_into(buf);
435 }
436}
437
438impl Aabb for TypstText {
439 fn aabb(&self) -> [glam::DVec3; 2] {
440 self.vitems.aabb()
441 }
442}
443
444impl ShiftTransform for TypstText {
445 fn shift(&mut self, shift: glam::DVec3) -> &mut Self {
446 self.vitems.shift(shift);
447 self
448 }
449}
450
451impl RotateTransform for TypstText {
452 fn rotate_on_axis(&mut self, axis: glam::DVec3, angle: f64) -> &mut Self {
453 self.vitems.rotate_on_axis(axis, angle);
454 self
455 }
456}
457
458impl ScaleTransform for TypstText {
459 fn scale(&mut self, scale: glam::DVec3) -> &mut Self {
460 self.vitems.scale(scale);
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 std::time::Instant;
556
557 use super::*;
558
559 #[test]
570 fn test_single_file_typst_world_foo() {
571 let start = Instant::now();
572 fonts();
573 println!("fonts search: {:?}", start.elapsed());
574
575 let start = Instant::now();
576 let world = TypstWorld::new();
577 println!("world construct: {:?}", start.elapsed());
578
579 let start = Instant::now();
580 let world = world.with_source_str("r");
581 println!("set source: {:?}", start.elapsed());
582
583 let start = Instant::now();
584 let document = typst::compile(&world)
585 .output
586 .expect("failed to compile typst source");
587 println!("document compile: {:?}", start.elapsed());
588
589 let start = Instant::now();
590 let svg = typst_svg::svg_merged(&document, Abs::pt(2.0));
591 println!("{svg}");
592 println!("svg output: {:?}", start.elapsed());
593
594 let start = Instant::now();
595 let res = get_typst_element(&svg);
596 println!("get element: {:?}", start.elapsed());
597
598 println!("{res}");
599 }
601
602 #[test]
623 fn foo_page() {
624 let text = r#"Ra"#;
625 let res = compile_typst_code(text);
626 println!("{res}");
627
628 let res = typst_svg(text);
629 println!("{res}");
630 }
631
632 #[test]
633 fn foo() {
634 let code_a = r#"#include <iostream>
635using namespace std;
636
637int main() {
638 cout << "Hello World!" << endl;
639}
640"#;
641 let mut code_a = TypstText::new_multiline_code(code_a, Some("cpp"));
642 let code_b = r#"fn main() {
643 println!("Hello World!");
644}"#;
645 let mut code_b = TypstText::new_multiline_code(code_b, Some("rust"));
646
647 code_a.align_with(&mut code_b);
648 }
649}