shiva/
typst.rs

1use crate::core::Element::{Header, Hyperlink, Image, List, Paragraph, Table, Text};
2
3use crate::core::{Document, Element, ListItem, TableHeader, TableRow, TransformerTrait};
4use anyhow;
5use bytes::Bytes;
6use comemo::Prehashed;
7use log::warn;
8use std::path::Path;
9use std::{collections::HashMap, io::Cursor};
10use time::{OffsetDateTime, UtcOffset};
11
12use typst::{
13    diag::{FileError, FileResult},
14    foundations::Datetime,
15    syntax::{FileId, Source},
16    text::{Font, FontBook},
17    Library, World,
18};
19
20type TypstString = String;
21
22pub struct ShivaWorld {
23    fonts: Vec<Font>,
24    book: Prehashed<FontBook>,
25    library: Prehashed<Library>,
26    source: Source,
27    img_map: HashMap<String, typst::foundations::Bytes>,
28}
29
30impl ShivaWorld {
31    pub fn new(source: String, img_map: HashMap<String, typst::foundations::Bytes>) -> Self {
32        let source = Source::detached(source);
33
34        let folder = "fonts";
35
36        // Check if the "fonts" folder exists
37        if !std::path::Path::new(folder).exists() {
38            // Create the "fonts" folder
39            std::fs::create_dir_all(folder).expect("Failed to create folder");
40
41            // Download fonts
42            let font_info = vec![
43                ("DejaVuSansMono-Bold.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/DejaVuSansMono-Bold.ttf"),
44                ("DejaVuSansMono.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/DejaVuSansMono.ttf"),
45                ("FiraMath-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/FiraMath-Regular.otf"),
46                ("IBMPlexSerif-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/IBMPlexSerif-Regular.ttf"),
47                ("InriaSerif-BoldItalic.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/InriaSerif-BoldItalic.ttf"),
48                ("InriaSerif-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/InriaSerif-Regular.ttf"),
49                ("LinLibertine_R.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_R.ttf"),
50                ("LinLibertine_RB.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_RB.ttf"),
51                ("LinLibertine_RBI.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_RBI.ttf"),
52                ("LinLibertine_RI.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_RI.ttf"),
53                ("Nerd.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/Nerd.ttf"),
54                ("NewCM10-Bold.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCM10-Bold.otf"),
55                ("NewCM10-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCM10-Regular.otf"),
56                ("NewCMMath-Book.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCMMath-Book.otf"),
57                ("NewCMMath-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCMMath-Regular.otf"),
58                ("NotoColorEmoji.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoColorEmoji.ttf"),
59                ("NotoSansArabic-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSansArabic-Regular.ttf"),
60                ("NotoSansSymbols2-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSansSymbols2-Regular.ttf"),
61                ("NotoSerifCJKsc-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSerifCJKsc-Regular.otf"),
62                ("NotoSerifHebrew-Bold.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSerifHebrew-Bold.ttf"),
63                ("NotoSerifHebrew-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSerifHebrew-Regular.ttf"),
64                ("PTSans-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/PTSans-Regular.ttf"),
65                ("Roboto-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/Roboto-Regular.ttf"),
66                ("TwitterColorEmoji.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/TwitterColorEmoji.ttf"),
67                ("Ubuntu-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/Ubuntu-Regular.ttf"),
68            ];
69
70            for (filename, url) in font_info {
71                download_font(url, folder, filename);
72            }
73        }
74
75        let fonts = std::fs::read_dir(folder)
76            .unwrap()
77            .map(Result::unwrap)
78            .flat_map(|entry| {
79                let path = entry.path();
80                let bytes = std::fs::read(&path).unwrap();
81                let buffer = typst::foundations::Bytes::from(bytes);
82                let face_count = ttf_parser::fonts_in_collection(&buffer).unwrap_or(1);
83                (0..face_count).map(move |face| {
84                    Font::new(buffer.clone(), face).unwrap_or_else(|| {
85                        panic!("failed to load font from {path:?} (face index {face})");
86                    })
87                })
88            })
89            .collect::<Vec<Font>>();
90
91        Self {
92            book: Prehashed::new(FontBook::from_fonts(&fonts)),
93            fonts,
94            library: Prehashed::new(Library::default()),
95            source,
96            img_map,
97        }
98    }
99}
100
101#[cfg(target_arch = "wasm32")]
102fn download_font(url: &str, folder: &str, filename: &str) {
103    use log::info;
104
105    let font_path = Path::new(folder).join(filename);
106
107    info!("Downloading font file {}...", font_path.display());
108
109    let request = ehttp::Request::get(url);
110    ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
111        let mut reader = Cursor::new(result.unwrap().bytes);
112        let f = std::fs::File::create(&font_path).unwrap();
113        let mut writer = std::io::BufWriter::new(f);
114
115        let _bytes_io_count = std::io::copy(&mut reader, &mut writer).unwrap();
116
117        info!("Font file {} downloaded successfully!", font_path.display());
118    });
119}
120
121#[cfg(not(target_arch = "wasm32"))]
122fn download_font(url: &str, folder: &str, filename: &str) {
123    use log::info;
124
125    let font_path = Path::new(folder).join(filename);
126
127    info!("Downloading font file {}...", font_path.display());
128
129    let request = ehttp::Request::get(url);
130    let response = ehttp::fetch_blocking(&request);
131    let mut reader = Cursor::new(response.unwrap().bytes);
132    let f = std::fs::File::create(&font_path).unwrap();
133    let mut writer = std::io::BufWriter::new(f);
134
135    let _bytes_io_count = std::io::copy(&mut reader, &mut writer).unwrap();
136
137    info!("Font file {} downloaded successfully!", font_path.display());
138}
139
140impl World for ShivaWorld {
141    fn book(&self) -> &Prehashed<FontBook> {
142        &self.book
143    }
144
145    fn library(&self) -> &Prehashed<Library> {
146        &self.library
147    }
148
149    fn main(&self) -> Source {
150        self.source.clone()
151    }
152
153    fn source(&self, _id: FileId) -> FileResult<Source> {
154        Ok(self.source.clone())
155    }
156
157    fn font(&self, id: usize) -> Option<Font> {
158        self.fonts.get(id).cloned()
159    }
160
161    // need to think how to implement path and file extraction
162    fn file(&self, id: FileId) -> Result<typst::foundations::Bytes, FileError> {
163        let path = id.vpath();
164
165        let key = path.as_rootless_path().to_str().unwrap();
166        let img = self.img_map.get(key).unwrap();
167
168        Ok(img.clone())
169    }
170
171    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
172        // We are in UTC.
173        let offset = offset.unwrap_or(0);
174        let offset = UtcOffset::from_hms(offset.try_into().ok()?, 0, 0).ok()?;
175        let time = OffsetDateTime::now_utc().checked_to_offset(offset)?;
176        Some(Datetime::Date(time.date()))
177    }
178}
179
180pub struct Transformer;
181
182impl TransformerTrait for Transformer {
183    #[allow(unused)]
184    fn parse(document: &bytes::Bytes) -> anyhow::Result<Document> {
185        todo!()
186    }
187
188    fn generate(document: &Document) -> anyhow::Result<bytes::Bytes> {
189        let (text, _) = generate_document(document)?;
190        let bytes = Bytes::from(text);
191        Ok(bytes)
192    }
193}
194
195/// Converts Document into a typst::model::Document
196pub fn generate_document(
197    document: &Document,
198) -> anyhow::Result<(TypstString, HashMap<String, typst::foundations::Bytes>)> {
199    // Array of methods to process Document object into a typst string repr
200    fn process_header(source: &mut TypstString, level: usize, text: &str) -> anyhow::Result<()> {
201        let header_depth = "=".repeat(level);
202        let header_text = format!("{header_depth} {text}");
203        source.push_str(&header_text);
204        source.push('\n');
205
206        Ok(())
207    }
208
209    fn process_text(
210        source: &mut TypstString,
211        _size: u8,
212        text: &str,
213        is_bold: bool,
214    ) -> anyhow::Result<()> {
215        if is_bold {
216            let bold_text = format!("*{text}*");
217            source.push_str(&bold_text);
218        } else {
219            source.push_str(text);
220        }
221
222        Ok(())
223    }
224
225    fn process_link(source: &mut TypstString, url: &str) -> anyhow::Result<()> {
226        let link = format!("#link(\"{url}\")");
227
228        source.push_str(&link);
229
230        Ok(())
231    }
232
233    fn process_table(
234        source: &mut TypstString,
235        headers: &Vec<TableHeader>,
236        rows: &Vec<TableRow>,
237    ) -> anyhow::Result<()> {
238        let mut headers_text = TypstString::new();
239
240        for header in headers {
241            match &header.element {
242                Text { text, size } => {
243                    headers_text.push('[');
244                    process_text(&mut headers_text, *size, text, true)?;
245                    headers_text.push(']');
246                    headers_text.push(',');
247                }
248                _ => {
249                    warn!(
250                        "Should implement element for processing in inside table header - {:?}",
251                        header.element
252                    );
253                }
254            }
255        }
256
257        let mut cells_text = TypstString::new();
258
259        for row in rows {
260            for cell in &row.cells {
261                match &cell.element {
262                    Text { text, size } => {
263                        cells_text.push('[');
264                        process_text(&mut cells_text, *size, text, false)?;
265                        cells_text.push(']');
266                        cells_text.push(',');
267                    }
268                    _ => {
269                        warn!(
270                            "Should implement element for processing in inside cell - {:?}",
271                            cell.element
272                        );
273                    }
274                }
275            }
276
277            cells_text.push('\n');
278        }
279
280        let columns = headers.len();
281        let table_text = format!(
282            r#"
283        #table(
284            columns:{columns},
285            {headers_text}
286            {cells_text}
287        )
288        "#
289        );
290
291        source.push_str(&table_text);
292        Ok(())
293    }
294
295    fn process_list(
296        source: &mut TypstString,
297        img_map: &mut HashMap<String, typst::foundations::Bytes>,
298        list: &Vec<ListItem>,
299        numbered: bool,
300        depth: usize,
301    ) -> anyhow::Result<()> {
302        source.push_str(&" ".repeat(depth));
303        for el in list {
304            if let List { elements, numbered } = &el.element {
305                process_list(source, img_map, elements, *numbered, depth + 1)?;
306            } else {
307                if numbered {
308                    source.push_str("+ ")
309                } else {
310                    source.push_str("- ")
311                };
312
313                process_element(source, img_map, &el.element)?;
314            }
315        }
316
317        Ok(())
318    }
319
320    fn process_image(
321        source: &mut TypstString,
322        bytes: &Bytes,
323        title: &str,
324        alt: &str,
325        image_type: &str,
326    ) -> anyhow::Result<()> {
327        if !bytes.is_empty() {
328            let image_text = format!(
329                "
330            #image(\"{title}{image_type}\", alt: \"{alt}\")
331            "
332            );
333            source.push_str(&image_text);
334        }
335        // need to think how to implement using raw bytes
336        Ok(())
337    }
338
339    fn process_element(
340        source: &mut TypstString,
341        img_map: &mut HashMap<String, typst::foundations::Bytes>,
342        element: &Element,
343    ) -> anyhow::Result<()> {
344        match element {
345            Header { level, text } => process_header(source, *level as usize, text),
346            Paragraph { elements } => {
347                for paragraph_element in elements {
348                    process_element(source, img_map, paragraph_element)?;
349                }
350
351                Ok(())
352            }
353            Text { text, size } => {
354                process_text(source, *size, text, false)?;
355                source.push('\n');
356
357                Ok(())
358            }
359            List { elements, numbered } => {
360                process_list(source, img_map, elements, *numbered, 0)?;
361                Ok(())
362            }
363            Hyperlink {
364                url,
365                title: _,
366                alt: _,
367                size: _,
368            } => {
369                process_link(source, url)?;
370                source.push('\n');
371
372                Ok(())
373            }
374            Table { headers, rows } => {
375                process_table(source, headers, rows)?;
376                Ok(())
377            }
378            Image(image) => {
379                let key = format!("{}{}", image.title(), image.image_type());
380                img_map.insert(key, typst::foundations::Bytes::from(image.bytes().to_vec()));
381                process_image(
382                    source,
383                    image.bytes(),
384                    image.title(),
385                    image.alt(),
386                    &image.image_type().to_string(),
387                )?;
388                source.push('\n');
389                Ok(())
390            } // _ => {
391              //     warn!("Should implement element - {:?}", element);
392              //     Ok(())
393              // }
394        }
395    }
396
397    // String to build off of
398    let mut source = TypstString::new();
399    // Mapping of connections between elements
400    let mut img_map: HashMap<String, typst::foundations::Bytes> = HashMap::new();
401
402    // Converting both headers and footers into a string repr of them in Typst
403    let mut header_text = String::new();
404    document.get_page_header().iter().for_each(|el| match el {
405        Text { text, size: _ } => {
406            header_text.push_str(text);
407        }
408        _ => {}
409    });
410    let mut footer_text = String::new();
411    document.get_page_footer().iter().for_each(|el| match el {
412        Text { text, size: _ } => {
413            footer_text.push_str(text);
414        }
415        _ => {}
416    });
417    let footer_header_text = format!(
418        "#set page(
419        header: \"{header_text}\",
420        footer: \"{footer_text}\",
421    )\n"
422    );
423
424    // Converting Document repr to one of typst string
425    source.push_str(&footer_header_text);
426    for element in &document.get_all_elements() {
427        process_element(&mut source, &mut img_map, element)?;
428    }
429
430    Ok((source, img_map))
431}
432
433#[cfg(test)]
434mod test {
435    use crate::core::{disk_image_loader, TransformerWithImageLoaderSaverTrait};
436    use crate::markdown;
437    use bytes::Bytes;
438
439    use super::*;
440    #[test]
441    fn test_generate() -> anyhow::Result<()> {
442        let document = std::fs::read("test/data/document.md")?;
443        let documents_bytes = Bytes::from(document);
444        let parsed = markdown::Transformer::parse_with_loader(
445            &documents_bytes,
446            disk_image_loader("test/data"),
447        )?;
448        let generated_result = crate::typst::Transformer::generate(&parsed)?;
449        std::fs::write("test/data/document_from_md.typ", generated_result)?;
450
451        Ok(())
452    }
453
454    #[test]
455    fn test_generate_from_xml() -> anyhow::Result<()> {
456        let document = std::fs::read("test/data/document.xml")?;
457        let documents_bytes = Bytes::from(document);
458        let parsed = crate::xml::Transformer::parse(&documents_bytes)?;
459        let generated_result = crate::typst::Transformer::generate(&parsed)?;
460        std::fs::write("test/data/document_from_xml.typ", generated_result)?;
461
462        Ok(())
463    }
464}