ranim_items/vitem/
typst.rs

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(&mut self, typst_str: &str) -> Option<&String> {
44    //     let mut sha1 = Sha1::new();
45    //     sha1.update(typst_str.as_bytes());
46    //     let sha1 = sha1.finalize();
47    //     self.inner.get::<[u8; 20]>(sha1.as_ref())
48    // }
49    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 = SingleFileTypstWorld::new(typst_str);
56                let world = typst_world().lock().unwrap();
57                let world = world.with_source_str(typst_str);
58                // world.set_source(typst_str);
59                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
88/// Compiles typst string to SVG string
89pub fn typst_svg(source: &str) -> String {
90    typst_lru().lock().unwrap().get_or_insert(source).clone()
91    // let world = SingleFileTypstWorld::new(source);
92    // let document = typst::compile(&world)
93    //     .output
94    //     .expect("failed to compile typst source");
95
96    // let svg = typst_svg::svg_merged(&document, Abs::pt(2.0));
97    // get_typst_element(&svg)
98}
99
100struct FileEntry {
101    bytes: Bytes,
102    /// This field is filled on demand.
103    source: Option<Source>,
104}
105
106impl FileEntry {
107    fn source(&mut self, id: FileId) -> FileResult<Source> {
108        // Fallible `get_or_insert`.
109        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            // Defuse the BOM!
114            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    // from https://github.com/mattfbacon/typst-bot
149    // TODO: package things
150    // Weird pattern because mapping a MutexGuard is not stable yet.
151    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        // `files` must stay locked here so we don't download the same package multiple times.
157        // TODO proper multithreading, maybe with typst-kit.
158
159        // 'x: {
160        // 	if let Some(package) = id.package() {
161        // 		let package_dir = self.ensure_package(package)?;
162        // 		let Some(path) = id.vpath().resolve(&package_dir) else {
163        // 			break 'x;
164        // 		};
165        // 		let contents = std::fs::read(&path).map_err(|error| FileError::from_io(error, &path))?;
166        // 		let entry = files.entry(id).or_insert(FileEntry {
167        // 			bytes: Bytes::new(contents),
168        // 			source: None,
169        // 		});
170        // 		return Ok(map(entry));
171        // 	}
172        // }
173
174        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/// A Text item construted through typst
230///
231/// Note that the methods this item provides assumes that the typst string
232/// you provide only produces text output, otherwise undefined behaviours may happens.
233#[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    /// Create a TypstText with typst string.
249    ///
250    /// The typst string you provide should only produces text output,
251    /// otherwise undefined behaviours may happens.
252    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    /// Inline code
266    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    /// Multiline code
280    pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
281        let language = language.unwrap_or("");
282        // Self::new(format!("```{language}\n{code}\n```").as_str())
283        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            // println!("[{ia}] {last_neq_idx_a} [{ib}] {last_neq_idx_b}");
357            // println!("{diff:?}");
358            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                // println!("{i} {}", a.is_aligned(b));
399                // println!("{} {}", a.vpoints.len(), b.vpoints.len());
400                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
517/// remove `r"<path[^>]*(?:>.*?<\/path>|\/>)"`
518pub 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    // println!("{}", String::from_utf8_lossy(&output));
523    // println!("{}", String::from_utf8_lossy(&removed_bg));
524    String::from_utf8_lossy(&removed_bg).to_string()
525}
526
527/// Compiles typst code to SVG string by spawning a typst process
528pub 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    /*
558    fonts search: 322.844709ms
559    world construct: 1.901541ms
560    set source: 958ns
561    file: 736
562    file: 818
563    document compile: 89.835583ms
564    svg output: 185.458µs
565    get element: 730.792µs
566     */
567    #[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        // println!("{}", typst_svg!(source))
597    }
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}