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},
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(&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(&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
94/// Compiles typst string to SVG string
95pub fn typst_svg(source: &str) -> String {
96    typst_lru().lock().unwrap().get_or_insert(source).clone()
97    // let world = SingleFileTypstWorld::new(source);
98    // let document = typst::compile(&world)
99    //     .output
100    //     .expect("failed to compile typst source");
101
102    // let svg = typst_svg::svg_merged(&document, Abs::pt(2.0));
103    // get_typst_element(&svg)
104}
105
106struct FileEntry {
107    bytes: Bytes,
108    /// This field is filled on demand.
109    source: Option<Source>,
110}
111
112impl FileEntry {
113    fn source(&mut self, id: FileId) -> FileResult<Source> {
114        // Fallible `get_or_insert`.
115        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            // Defuse the BOM!
120            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    // from https://github.com/mattfbacon/typst-bot
155    // TODO: package things
156    // Weird pattern because mapping a MutexGuard is not stable yet.
157    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        // `files` must stay locked here so we don't download the same package multiple times.
163        // TODO proper multithreading, maybe with typst-kit.
164
165        // 'x: {
166        // 	if let Some(package) = id.package() {
167        // 		let package_dir = self.ensure_package(package)?;
168        // 		let Some(path) = id.vpath().resolve(&package_dir) else {
169        // 			break 'x;
170        // 		};
171        // 		let contents = std::fs::read(&path).map_err(|error| FileError::from_io(error, &path))?;
172        // 		let entry = files.entry(id).or_insert(FileEntry {
173        // 			bytes: Bytes::new(contents),
174        // 			source: None,
175        // 		});
176        // 		return Ok(map(entry));
177        // 	}
178        // }
179
180        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/// A Text item construted through typst
236///
237/// Note that the methods this item provides assumes that the typst string
238/// you provide only produces text output, otherwise undefined behaviours may happens.
239#[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    /// Create a TypstText with typst string.
255    ///
256    /// The typst string you provide should only produces text output,
257    /// otherwise undefined behaviours may happens.
258    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    /// Inline code
272    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    /// Multiline code
286    pub fn new_multiline_code(code: &str, language: Option<&str>) -> Self {
287        let language = language.unwrap_or("");
288        // Self::new(format!("```{language}\n{code}\n```").as_str())
289        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            // 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 = 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
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 std::time::Instant;
556
557    use super::*;
558
559    /*
560    fonts search: 322.844709ms
561    world construct: 1.901541ms
562    set source: 958ns
563    file: 736
564    file: 818
565    document compile: 89.835583ms
566    svg output: 185.458µs
567    get element: 730.792µs
568     */
569    #[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        // println!("{}", typst_svg!(source))
600    }
601
602    ///
603    /// ```
604    /// <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">
605    ///    <path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0v 11 h 11.484 v -11 Z "/>
606    ///    <g>
607    ///        <g class="typst-text" transform="matrix(1 0 0 -1 0 11)">
608    ///            <use xlink:href="#gB5279FC30F2C6542A76CE0CDC73F9462" x="0" y="0" fill="#000000" fill-rule="nonzero"/>
609    ///            <use xlink:href="#gC5A0A6F735BE491513D9F5FD3BD367ED" x="6.457" y="0" fill="#000000" fill-rule="nonzero"/>
610    ///        </g>
611    ///    </g>
612    /// ```
613    /// ```
614    /// <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">
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    #[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}