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::{Alignable, ApplyTransform, FillColor, Opacity, StrokeColor, StrokeWidth, With},
34};
35
36struct TypstLruCache {
37    inner: LruCache<[u8; 20], String>,
38}
39
40impl TypstLruCache {
41    fn new(cap: NonZeroUsize) -> Self {
42        Self {
43            inner: LruCache::new(cap),
44        }
45    }
46    // fn get(&mut self, typst_str: &str) -> Option<&String> {
47    //     let mut sha1 = Sha1::new();
48    //     sha1.update(typst_str.as_bytes());
49    //     let sha1 = sha1.finalize();
50    //     self.inner.get::<[u8; 20]>(sha1.as_ref())
51    // }
52    fn get_or_insert(&mut self, typst_str: &str) -> &String {
53        let mut sha1 = Sha1::new();
54        sha1.update(typst_str.as_bytes());
55        let sha1 = sha1.finalize();
56        self.inner
57            .get_or_insert_ref(AsRef::<[u8; 20]>::as_ref(&sha1), || {
58                // let world = SingleFileTypstWorld::new(typst_str);
59                let world = typst_world().lock().unwrap();
60                let world = world.with_source_str(typst_str);
61                // world.set_source(typst_str);
62                let document = typst::compile(&world)
63                    .output
64                    .expect("failed to compile typst source");
65
66                let svg = typst_svg::svg_merged(
67                    &document,
68                    &typst_svg::SvgOptions::default(),
69                    Abs::pt(2.0),
70                );
71                get_typst_element(&svg)
72            })
73    }
74}
75
76fn typst_lru() -> &'static Arc<Mutex<TypstLruCache>> {
77    static LRU: OnceLock<Arc<Mutex<TypstLruCache>>> = OnceLock::new();
78    LRU.get_or_init(|| {
79        Arc::new(Mutex::new(TypstLruCache::new(
80            NonZeroUsize::new(256).unwrap(),
81        )))
82    })
83}
84
85fn fonts() -> &'static FontStore {
86    static FONTS: OnceLock<FontStore> = OnceLock::new();
87    FONTS.get_or_init(|| {
88        let mut fonts = FontStore::new();
89        fonts.extend(typst_kit::fonts::embedded());
90        fonts.extend(typst_kit::fonts::system());
91        fonts
92    })
93}
94
95fn typst_world() -> &'static Arc<Mutex<TypstWorld>> {
96    static WORLD: OnceLock<Arc<Mutex<TypstWorld>>> = OnceLock::new();
97    WORLD.get_or_init(|| Arc::new(Mutex::new(TypstWorld::new())))
98}
99
100/// Compiles typst string to SVG string
101pub fn typst_svg(source: &str) -> String {
102    typst_lru().lock().unwrap().get_or_insert(source).clone()
103    // let world = SingleFileTypstWorld::new(source);
104    // let document = typst::compile(&world)
105    //     .output
106    //     .expect("failed to compile typst source");
107
108    // let svg = typst_svg::svg_merged(&document, Abs::pt(2.0));
109    // get_typst_element(&svg)
110}
111
112struct FileEntry {
113    bytes: Bytes,
114    /// This field is filled on demand.
115    source: Option<Source>,
116}
117
118impl FileEntry {
119    fn source(&mut self, id: FileId) -> FileResult<Source> {
120        // Fallible `get_or_insert`.
121        let source = if let Some(source) = &self.source {
122            source
123        } else {
124            let contents = std::str::from_utf8(&self.bytes).map_err(|_| FileError::InvalidUtf8)?;
125            // Defuse the BOM!
126            let contents = contents.trim_start_matches('\u{feff}');
127            let source = Source::new(id, contents.into());
128            self.source.insert(source)
129        };
130        Ok(source.clone())
131    }
132}
133
134pub(crate) struct TypstWorld {
135    library: LazyHash<Library>,
136    book: LazyHash<FontBook>,
137    files: Mutex<HashMap<FileId, FileEntry>>,
138}
139
140impl TypstWorld {
141    pub(crate) fn new() -> Self {
142        let fonts = fonts();
143        Self {
144            library: LazyHash::new(Library::default()),
145            book: fonts.book().clone(),
146            files: Mutex::new(HashMap::new()),
147        }
148    }
149    pub(crate) fn with_source_str(&self, source: &str) -> TypstWorldWithSource<'_> {
150        self.with_source(Source::detached(source))
151    }
152    pub(crate) fn with_source(&self, source: Source) -> TypstWorldWithSource<'_> {
153        TypstWorldWithSource {
154            world: self,
155            source,
156            now: OnceLock::new(),
157        }
158    }
159
160    // from https://github.com/mattfbacon/typst-bot
161    // TODO: package things
162    // Weird pattern because mapping a MutexGuard is not stable yet.
163    fn file<T>(&self, id: FileId, map: impl FnOnce(&mut FileEntry) -> T) -> FileResult<T> {
164        let mut files = self.files.lock().unwrap();
165        if let Some(entry) = files.get_mut(&id) {
166            return Ok(map(entry));
167        }
168        // `files` must stay locked here so we don't download the same package multiple times.
169        // TODO proper multithreading, maybe with typst-kit.
170
171        // 'x: {
172        // 	if let Some(package) = id.package() {
173        // 		let package_dir = self.ensure_package(package)?;
174        // 		let Some(path) = id.vpath().resolve(&package_dir) else {
175        // 			break 'x;
176        // 		};
177        // 		let contents = std::fs::read(&path).map_err(|error| FileError::from_io(error, &path))?;
178        // 		let entry = files.entry(id).or_insert(FileEntry {
179        // 			bytes: Bytes::new(contents),
180        // 			source: None,
181        // 		});
182        // 		return Ok(map(entry));
183        // 	}
184        // }
185
186        Err(FileError::NotFound(id.vpath().get_without_slash().into()))
187    }
188}
189
190pub(crate) struct TypstWorldWithSource<'a> {
191    world: &'a TypstWorld,
192    source: Source,
193    now: OnceLock<DateTime<Local>>,
194}
195
196impl World for TypstWorldWithSource<'_> {
197    fn library(&self) -> &LazyHash<Library> {
198        &self.world.library
199    }
200
201    fn book(&self) -> &LazyHash<FontBook> {
202        &self.world.book
203    }
204
205    fn main(&self) -> FileId {
206        self.source.id()
207    }
208
209    fn source(&self, id: FileId) -> FileResult<Source> {
210        if id == self.source.id() {
211            Ok(self.source.clone())
212        } else {
213            self.world.file(id, |entry| entry.source(id))?
214        }
215    }
216
217    fn file(&self, id: FileId) -> FileResult<Bytes> {
218        self.world.file(id, |file| file.bytes.clone())
219    }
220
221    fn font(&self, index: usize) -> Option<Font> {
222        fonts().font(index)
223    }
224
225    fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
226        let now = self.now.get_or_init(chrono::Local::now);
227
228        let naive = match offset {
229            None => now.naive_local(),
230            Some(o) => now.naive_utc() + chrono::Duration::seconds(o.seconds() as i64),
231        };
232
233        Datetime::from_ymd(
234            naive.year(),
235            naive.month().try_into().ok()?,
236            naive.day().try_into().ok()?,
237        )
238    }
239}
240
241/// A Text item construted through typst
242///
243/// Note that the methods this item provides assumes that the typst string
244/// you provide only produces text output, otherwise undefined behaviours may happens.
245#[derive(Clone)]
246pub struct TypstText {
247    chars: String,
248    vitems: Vec<VItem>,
249}
250
251impl TypstText {
252    fn _new(str: &str) -> Self {
253        let svg = SvgItem::new(typst_svg(str));
254        let chars = str.to_string();
255
256        let vitems = Vec::<VItem>::from(svg);
257        assert_eq!(chars.len(), vitems.len());
258        Self { chars, vitems }
259    }
260    /// Create a TypstText with typst string.
261    ///
262    /// The typst string you provide should only produces text output,
263    /// otherwise undefined behaviours may happens.
264    pub fn new(typst_str: &str) -> Self {
265        let svg = SvgItem::new(typst_svg(typst_str));
266        let chars = typst_str
267            .replace(" ", "")
268            .replace("\n", "")
269            .replace("\r", "")
270            .replace("\t", "");
271
272        let vitems = Vec::<VItem>::from(svg);
273        assert_eq!(chars.len(), vitems.len());
274        Self { chars, vitems }
275    }
276
277    /// Inline code
278    pub fn new_inline_code(code: &str) -> Self {
279        let svg = SvgItem::new(typst_svg(format!("`{code}`").as_str()));
280        let chars = code
281            .replace(" ", "")
282            .replace("\n", "")
283            .replace("\r", "")
284            .replace("\t", "");
285
286        let vitems = Vec::<VItem>::from(svg);
287        assert_eq!(chars.len(), vitems.len());
288        Self { chars, vitems }
289    }
290
291    /// Multiline code
292    pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
293        let language = language.unwrap_or("");
294        // Self::new(format!("```{language}\n{code}\n```").as_str())
295        let svg = SvgItem::new(typst_svg(format!("```{language}\n{code}```").as_str()));
296        let chars = code
297            .replace(" ", "")
298            .replace("\n", "")
299            .replace("\r", "")
300            .replace("\t", "");
301
302        let vitems = Vec::<VItem>::from(svg);
303        assert_eq!(chars.len(), vitems.len());
304        Self { chars, vitems }
305    }
306}
307
308impl Alignable for TypstText {
309    fn is_aligned(&self, other: &Self) -> bool {
310        self.vitems.len() == other.vitems.len()
311            && self
312                .vitems
313                .iter()
314                .zip(&other.vitems)
315                .all(|(a, b)| a.is_aligned(b))
316    }
317    fn align_with(&mut self, other: &mut Self) {
318        let dmp = diff_match_patch_rs::DiffMatchPatch::new();
319        let diffs = dmp
320            .diff_main::<Efficient>(&self.chars, &other.chars)
321            .unwrap();
322
323        let len = self.vitems.len().max(other.vitems.len());
324        let mut vitems_self: Vec<VItem> = Vec::with_capacity(len);
325        let mut vitems_other: Vec<VItem> = Vec::with_capacity(len);
326        let mut ia = 0;
327        let mut ib = 0;
328        let mut last_neq_idx_a = 0;
329        let mut last_neq_idx_b = 0;
330        let align_and_push_diff = |vitems_self: &mut Vec<VItem>,
331                                   vitems_other: &mut Vec<VItem>,
332                                   ia,
333                                   ib,
334                                   last_neq_idx_a,
335                                   last_neq_idx_b| {
336            if last_neq_idx_a != ia || last_neq_idx_b != ib {
337                let mut vitems_a = self.vitems[last_neq_idx_a..ia].to_vec();
338                let mut vitems_b = other.vitems[last_neq_idx_b..ib].to_vec();
339                if vitems_a.is_empty() {
340                    vitems_a.extend(vitems_b.iter().map(|x| {
341                        x.clone().with(|x| {
342                            x.shrink();
343                        })
344                    }));
345                }
346                if vitems_b.is_empty() {
347                    vitems_b.extend(vitems_a.iter().map(|x| {
348                        x.clone().with(|x| {
349                            x.shrink();
350                        })
351                    }));
352                }
353                if last_neq_idx_a != ia && last_neq_idx_b != ib {
354                    vitems_a.align_with(&mut vitems_b);
355                }
356                vitems_self.extend(vitems_a);
357                vitems_other.extend(vitems_b);
358            }
359        };
360
361        for diff in &diffs {
362            // println!("[{ia}] {last_neq_idx_a} [{ib}] {last_neq_idx_b}");
363            // println!("{diff:?}");
364            match diff.op() {
365                Ops::Equal => {
366                    align_and_push_diff(
367                        &mut vitems_self,
368                        &mut vitems_other,
369                        ia,
370                        ib,
371                        last_neq_idx_a,
372                        last_neq_idx_b,
373                    );
374                    let l = diff.size();
375                    vitems_self.extend(self.vitems[ia..ia + l].iter().cloned());
376                    vitems_other.extend(other.vitems[ib..ib + l].iter().cloned());
377                    ia += l;
378                    ib += l;
379                    last_neq_idx_a = ia;
380                    last_neq_idx_b = ib;
381                }
382                Ops::Delete => {
383                    ia += diff.size();
384                }
385                Ops::Insert => {
386                    ib += diff.size();
387                }
388            }
389        }
390        align_and_push_diff(
391            &mut vitems_self,
392            &mut vitems_other,
393            ia,
394            ib,
395            last_neq_idx_a,
396            last_neq_idx_b,
397        );
398
399        assert_eq!(vitems_self.len(), vitems_other.len());
400        vitems_self
401            .iter_mut()
402            .zip(vitems_other.iter_mut())
403            .for_each(|(a, b)| {
404                // println!("{i} {}", a.is_aligned(b));
405                // println!("{} {}", a.vpoints.len(), b.vpoints.len());
406                if !a.is_aligned(b) {
407                    a.align_with(b);
408                }
409            });
410
411        self.vitems = vitems_self;
412        other.vitems = vitems_other;
413    }
414}
415
416impl Interpolatable for TypstText {
417    fn lerp(&self, target: &Self, t: f64) -> Self {
418        let vitems = self
419            .vitems
420            .iter()
421            .zip(&target.vitems)
422            .map(|(a, b)| a.lerp(b, t))
423            .collect::<Vec<_>>();
424        Self {
425            chars: self.chars.clone(),
426            vitems,
427        }
428    }
429}
430
431impl From<TypstText> for Vec<VItem> {
432    fn from(value: TypstText) -> Self {
433        value.vitems
434    }
435}
436
437impl Extract for TypstText {
438    type Target = CoreItem;
439    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
440        self.vitems.extract_into(buf);
441    }
442}
443
444impl Aabb for TypstText {
445    fn aabb(&self) -> [glam::DVec3; 2] {
446        self.vitems.aabb()
447    }
448}
449
450impl<G: Into<glam::DAffine3>> ApplyTransform<G> for TypstText {
451    fn apply(&mut self, transform: G) -> &mut Self {
452        self.vitems.apply(transform.into());
453        self
454    }
455}
456
457impl FillColor for TypstText {
458    fn fill_color(&self) -> color::AlphaColor<color::Srgb> {
459        self.vitems[0].fill_color()
460    }
461    fn set_fill_color(&mut self, color: color::AlphaColor<color::Srgb>) -> &mut Self {
462        self.vitems.set_fill_color(color);
463        self
464    }
465    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
466        self.vitems.set_fill_opacity(opacity);
467        self
468    }
469}
470
471impl StrokeColor for TypstText {
472    fn stroke_color(&self) -> color::AlphaColor<color::Srgb> {
473        self.vitems[0].fill_color()
474    }
475    fn set_stroke_color(&mut self, color: color::AlphaColor<color::Srgb>) -> &mut Self {
476        self.vitems.set_stroke_color(color);
477        self
478    }
479    fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
480        self.vitems.set_stroke_opacity(opacity);
481        self
482    }
483}
484
485impl Opacity for TypstText {
486    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
487        self.vitems.set_fill_opacity(opacity);
488        self.vitems.set_stroke_opacity(opacity);
489        self
490    }
491}
492
493impl StrokeWidth for TypstText {
494    fn stroke_width(&self) -> f32 {
495        self.vitems.stroke_width()
496    }
497    fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
498        self.vitems.iter_mut().for_each(|vitem| {
499            vitem.apply_stroke_func(&f);
500        });
501        self
502    }
503    fn set_stroke_width(&mut self, width: f32) -> &mut Self {
504        self.vitems.set_stroke_width(width);
505        self
506    }
507}
508
509/// remove `r"<path[^>]*(?:>.*?<\/path>|\/>)"`
510pub fn get_typst_element(svg: &str) -> String {
511    let re = Regex::new(r"<path[^>]*(?:>.*?<\/path>|\/>)").unwrap();
512    let removed_bg = re.replace(svg.as_bytes(), b"");
513    let re = Regex::new(r#"\s+(?:viewBox|width|height)="[^"]*""#).unwrap();
514    let removed_size = re.replace_all(&removed_bg, b"");
515
516    // println!("{}", String::from_utf8_lossy(&output));
517    // println!("{}", String::from_utf8_lossy(&removed_bg));
518    String::from_utf8_lossy(&removed_size).to_string()
519}
520
521/// Compiles typst code to SVG string by spawning a typst process
522pub fn compile_typst_code(typst_code: &str) -> String {
523    let mut child = std::process::Command::new("typst")
524        .arg("compile")
525        .arg("-")
526        .arg("-")
527        .arg("-fsvg")
528        .stdin(std::process::Stdio::piped())
529        .stdout(std::process::Stdio::piped())
530        .spawn()
531        .expect("failed to spawn typst");
532
533    if let Some(mut stdin) = child.stdin.take() {
534        stdin
535            .write_all(typst_code.as_bytes())
536            .expect("failed to write to typst's stdin");
537    }
538
539    let output = child.wait_with_output().unwrap().stdout;
540    let output = String::from_utf8_lossy(&output);
541
542    output.to_string()
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548
549    #[test]
550    fn typst_element_strips_background_paths_and_canvas_size() {
551        let svg = r#"<svg viewBox="0 0 5 5" width="5pt" height="5pt"><path d="M0 0"/><g/></svg>"#;
552        assert_eq!(get_typst_element(svg), "<svg><g/></svg>");
553    }
554
555    #[test]
556    fn typst_svg_compiles_and_is_cached() {
557        let svg = typst_svg("R");
558        assert!(svg.contains("<svg"), "unexpected typst svg: {svg}");
559        assert_eq!(svg, typst_svg("R"), "cached compiles must be deterministic");
560    }
561}