Skip to main content

dioxus_typst/
lib.rs

1use std::collections::HashMap;
2
3use chrono::{Datelike, Timelike};
4use dioxus::prelude::*;
5use typst::{
6    Feature, Library, LibraryExt, World,
7    diag::{FileError, FileResult},
8    foundations::{Bytes, Datetime},
9    syntax::{FileId, Source, VirtualPath},
10    text::{Font, FontBook},
11    utils::LazyHash,
12};
13use typst_html::HtmlDocument;
14
15#[derive(Debug, Clone, Default, PartialEq)]
16pub struct CompileOptions {
17    pub files: HashMap<String, Vec<u8>>,
18}
19
20impl CompileOptions {
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    pub fn with_file(mut self, path: impl Into<String>, content: Vec<u8>) -> Self {
26        self.files.insert(path.into(), content);
27        self
28    }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum CompileError {
33    Typst(String),
34}
35
36impl std::fmt::Display for CompileError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            CompileError::Typst(msg) => write!(f, "Typst compilation error: {msg}"),
40        }
41    }
42}
43
44impl std::error::Error for CompileError {}
45
46struct CompileWorld {
47    library: LazyHash<Library>,
48    book: LazyHash<FontBook>,
49    fonts: Vec<Font>,
50    main: Source,
51    files: HashMap<String, Bytes>,
52}
53
54impl CompileWorld {
55    fn new(source: &str, options: &CompileOptions) -> Self {
56        let fonts = load_fonts();
57        let book = FontBook::from_fonts(&fonts);
58        let main_id = FileId::new(None, VirtualPath::new("/main.typ"));
59        let main = Source::new(main_id, source.to_string());
60        let files = options
61            .files
62            .iter()
63            .map(|(path, content)| (path.clone(), Bytes::new(content.clone())))
64            .collect();
65        let library = Library::builder()
66            .with_features([Feature::Html].into_iter().collect())
67            .build();
68
69        Self {
70            library: LazyHash::new(library),
71            book: LazyHash::new(book),
72            fonts,
73            main,
74            files,
75        }
76    }
77}
78
79impl World for CompileWorld {
80    fn library(&self) -> &LazyHash<Library> {
81        &self.library
82    }
83
84    fn book(&self) -> &LazyHash<FontBook> {
85        &self.book
86    }
87
88    fn main(&self) -> FileId {
89        self.main.id()
90    }
91
92    fn source(&self, id: FileId) -> FileResult<Source> {
93        if id == self.main.id() {
94            Ok(self.main.clone())
95        } else {
96            Err(FileError::NotFound(id.vpath().as_rooted_path().into()))
97        }
98    }
99
100    fn file(&self, id: FileId) -> FileResult<Bytes> {
101        let path = id.vpath().as_rooted_path().to_string_lossy();
102        self.files
103            .get(path.as_ref())
104            .cloned()
105            .ok_or_else(|| FileError::NotFound(id.vpath().as_rooted_path().into()))
106    }
107
108    fn font(&self, index: usize) -> Option<Font> {
109        self.fonts.get(index).cloned()
110    }
111
112    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
113        let now = chrono::Local::now();
114        let now = match offset {
115            Some(hours) => {
116                let offset = chrono::FixedOffset::east_opt((hours * 3600) as i32)?;
117                now.with_timezone(&offset).naive_local()
118            }
119            None => now.naive_local(),
120        };
121        Datetime::from_ymd_hms(
122            now.year(),
123            now.month().try_into().ok()?,
124            now.day().try_into().ok()?,
125            now.hour().try_into().ok()?,
126            now.minute().try_into().ok()?,
127            now.second().try_into().ok()?,
128        )
129    }
130}
131
132fn load_fonts() -> Vec<Font> {
133    let mut fonts = Vec::new();
134    #[cfg(feature = "fonts")]
135    for data in typst_assets::fonts() {
136        for font in Font::iter(Bytes::new(data)) {
137            fonts.push(font);
138        }
139    }
140    fonts
141}
142
143fn compile(source: &str, options: &CompileOptions) -> Result<String, CompileError> {
144    let world = CompileWorld::new(source, options);
145    let warned = typst::compile::<HtmlDocument>(&world);
146    let document = warned.output.map_err(|errors| {
147        let messages: Vec<String> = errors.iter().map(|e| e.message.to_string()).collect();
148        CompileError::Typst(messages.join("; "))
149    })?;
150    typst_html::html(&document).map_err(|errors| {
151        let messages: Vec<String> = errors.iter().map(|e| e.message.to_string()).collect();
152        CompileError::Typst(messages.join("; "))
153    })
154}
155
156#[component]
157pub fn Typst(
158    source: String,
159    #[props(default)] options: CompileOptions,
160    #[props(default = "typst-content".to_string())] class: String,
161) -> Element {
162    match compile(&source, &options) {
163        Ok(html) => rsx! {
164            div { class, dangerous_inner_html: "{html}" }
165        },
166        Err(e) => rsx! {
167            div { class: "typst-error", "Error compiling Typst: {e}" }
168        },
169    }
170}