Skip to main content

ranim_items/vitem/
typst.rs

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