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 if !std::path::Path::new(folder).exists() {
38 std::fs::create_dir_all(folder).expect("Failed to create folder");
40
41 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 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 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
195pub fn generate_document(
197 document: &Document,
198) -> anyhow::Result<(TypstString, HashMap<String, typst::foundations::Bytes>)> {
199 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 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 } }
395 }
396
397 let mut source = TypstString::new();
399 let mut img_map: HashMap<String, typst::foundations::Bytes> = HashMap::new();
401
402 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 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}