Skip to main content

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, 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(&mut self, typst_str: &str) -> Option<&String> {
50    //     let mut sha1 = Sha1::new();
51    //     sha1.update(typst_str.as_bytes());
52    //     let sha1 = sha1.finalize();
53    //     self.inner.get::<[u8; 20]>(sha1.as_ref())
54    // }
55    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 = SingleFileTypstWorld::new(typst_str);
62                let world = typst_world().lock().unwrap();
63                let world = world.with_source_str(typst_str);
64                // world.set_source(typst_str);
65                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
103/// Compiles typst string to SVG string
104pub fn typst_svg(source: &str) -> String {
105    typst_lru().lock().unwrap().get_or_insert(source).clone()
106    // let world = SingleFileTypstWorld::new(source);
107    // let document = typst::compile(&world)
108    //     .output
109    //     .expect("failed to compile typst source");
110
111    // let svg = typst_svg::svg_merged(&document, Abs::pt(2.0));
112    // get_typst_element(&svg)
113}
114
115struct FileEntry {
116    bytes: Bytes,
117    /// This field is filled on demand.
118    source: Option<Source>,
119}
120
121impl FileEntry {
122    fn source(&mut self, id: FileId) -> FileResult<Source> {
123        // Fallible `get_or_insert`.
124        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            // Defuse the BOM!
129            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    // from https://github.com/mattfbacon/typst-bot
164    // TODO: package things
165    // Weird pattern because mapping a MutexGuard is not stable yet.
166    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        // `files` must stay locked here so we don't download the same package multiple times.
172        // TODO proper multithreading, maybe with typst-kit.
173
174        // 'x: {
175        // 	if let Some(package) = id.package() {
176        // 		let package_dir = self.ensure_package(package)?;
177        // 		let Some(path) = id.vpath().resolve(&package_dir) else {
178        // 			break 'x;
179        // 		};
180        // 		let contents = std::fs::read(&path).map_err(|error| FileError::from_io(error, &path))?;
181        // 		let entry = files.entry(id).or_insert(FileEntry {
182        // 			bytes: Bytes::new(contents),
183        // 			source: None,
184        // 		});
185        // 		return Ok(map(entry));
186        // 	}
187        // }
188
189        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/// A Text item construted through typst
245///
246/// Note that the methods this item provides assumes that the typst string
247/// you provide only produces text output, otherwise undefined behaviours may happens.
248#[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    /// Create a TypstText with typst string.
264    ///
265    /// The typst string you provide should only produces text output,
266    /// otherwise undefined behaviours may happens.
267    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    /// Inline code
281    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    /// Multiline code
295    pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
296        let language = language.unwrap_or("");
297        // Self::new(format!("```{language}\n{code}\n```").as_str())
298        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            // println!("[{ia}] {last_neq_idx_a} [{ib}] {last_neq_idx_b}");
366            // println!("{diff:?}");
367            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                // println!("{i} {}", a.is_aligned(b));
408                // println!("{} {}", a.vpoints.len(), b.vpoints.len());
409                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
526/// remove `r"<path[^>]*(?:>.*?<\/path>|\/>)"`
527pub 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    // println!("{}", String::from_utf8_lossy(&output));
534    // println!("{}", String::from_utf8_lossy(&removed_bg));
535    String::from_utf8_lossy(&removed_size).to_string()
536}
537
538/// Compiles typst code to SVG string by spawning a typst process
539pub 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    /*
569    fonts search: 322.844709ms
570    world construct: 1.901541ms
571    set source: 958ns
572    file: 736
573    file: 818
574    document compile: 89.835583ms
575    svg output: 185.458µs
576    get element: 730.792µs
577     */
578    #[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        // println!("{}", typst_svg!(source))
609    }
610
611    ///
612    /// ```
613    /// <svg class="typst-doc" viewBox="0 0 11.483999999999998 11" width="11.483999999999998pt" height="11pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
614    ///    <path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0v 11 h 11.484 v -11 Z "/>
615    ///    <g>
616    ///        <g class="typst-text" transform="matrix(1 0 0 -1 0 11)">
617    ///            <use xlink:href="#gB5279FC30F2C6542A76CE0CDC73F9462" x="0" y="0" fill="#000000" fill-rule="nonzero"/>
618    ///            <use xlink:href="#gC5A0A6F735BE491513D9F5FD3BD367ED" x="6.457" y="0" fill="#000000" fill-rule="nonzero"/>
619    ///        </g>
620    ///    </g>
621    /// ```
622    /// ```
623    /// <svg class="typst-doc" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
624    /// <g>
625    ///     <g class="typst-text" transform="matrix(1 0 0 -1 0 11)">
626    ///         <use xlink:href="#gB5279FC30F2C6542A76CE0CDC73F9462" x="0" y="0" fill="#000000" fill-rule="nonzero"/>
627    ///         <use xlink:href="#gC5A0A6F735BE491513D9F5FD3BD367ED" x="6.457" y="0" fill="#000000" fill-rule="nonzero"/>
628    ///     </g>
629    /// </g>
630    /// ```
631    #[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}