use crate::core::Element::{Header, Hyperlink, Image, List, Paragraph, Table, Text};
use crate::core::{Document, Element, ListItem, TableHeader, TableRow, TransformerTrait};
use anyhow;
use bytes::Bytes;
use comemo::Prehashed;
use log::warn;
use std::path::Path;
use std::{collections::HashMap, io::Cursor};
use time::{OffsetDateTime, UtcOffset};
use typst::{
diag::{FileError, FileResult},
foundations::Datetime,
syntax::{FileId, Source},
text::{Font, FontBook},
Library, World,
};
type TypstString = String;
pub struct ShivaWorld {
fonts: Vec<Font>,
book: Prehashed<FontBook>,
library: Prehashed<Library>,
source: Source,
img_map: HashMap<String, typst::foundations::Bytes>,
}
impl ShivaWorld {
pub fn new(source: String, img_map: HashMap<String, typst::foundations::Bytes>) -> Self {
let source = Source::detached(source);
let folder = "fonts";
if !std::path::Path::new(folder).exists() {
std::fs::create_dir_all(folder).expect("Failed to create folder");
let font_info = vec![
("DejaVuSansMono-Bold.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/DejaVuSansMono-Bold.ttf"),
("DejaVuSansMono.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/DejaVuSansMono.ttf"),
("FiraMath-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/FiraMath-Regular.otf"),
("IBMPlexSerif-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/IBMPlexSerif-Regular.ttf"),
("InriaSerif-BoldItalic.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/InriaSerif-BoldItalic.ttf"),
("InriaSerif-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/InriaSerif-Regular.ttf"),
("LinLibertine_R.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_R.ttf"),
("LinLibertine_RB.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_RB.ttf"),
("LinLibertine_RBI.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_RBI.ttf"),
("LinLibertine_RI.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/LinLibertine_RI.ttf"),
("Nerd.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/Nerd.ttf"),
("NewCM10-Bold.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCM10-Bold.otf"),
("NewCM10-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCM10-Regular.otf"),
("NewCMMath-Book.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCMMath-Book.otf"),
("NewCMMath-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NewCMMath-Regular.otf"),
("NotoColorEmoji.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoColorEmoji.ttf"),
("NotoSansArabic-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSansArabic-Regular.ttf"),
("NotoSansSymbols2-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSansSymbols2-Regular.ttf"),
("NotoSerifCJKsc-Regular.otf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSerifCJKsc-Regular.otf"),
("NotoSerifHebrew-Bold.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSerifHebrew-Bold.ttf"),
("NotoSerifHebrew-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/NotoSerifHebrew-Regular.ttf"),
("PTSans-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/PTSans-Regular.ttf"),
("Roboto-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/Roboto-Regular.ttf"),
("TwitterColorEmoji.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/TwitterColorEmoji.ttf"),
("Ubuntu-Regular.ttf", "https://github.com/igumnoff/shiva/raw/main/lib/fonts/Ubuntu-Regular.ttf"),
];
for (filename, url) in font_info {
download_font(url, folder, filename);
}
}
let fonts = std::fs::read_dir(folder)
.unwrap()
.map(Result::unwrap)
.flat_map(|entry| {
let path = entry.path();
let bytes = std::fs::read(&path).unwrap();
let buffer = typst::foundations::Bytes::from(bytes);
let face_count = ttf_parser::fonts_in_collection(&buffer).unwrap_or(1);
(0..face_count).map(move |face| {
Font::new(buffer.clone(), face).unwrap_or_else(|| {
panic!("failed to load font from {path:?} (face index {face})");
})
})
})
.collect::<Vec<Font>>();
Self {
book: Prehashed::new(FontBook::from_fonts(&fonts)),
fonts,
library: Prehashed::new(Library::default()),
source,
img_map,
}
}
}
#[cfg(target_arch = "wasm32")]
fn download_font(url: &str, folder: &str, filename: &str) {
use log::info;
let font_path = Path::new(folder).join(filename);
info!("Downloading font file {}...", font_path.display());
let request = ehttp::Request::get(url);
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let mut reader = Cursor::new(result.unwrap().bytes);
let f = std::fs::File::create(&font_path).unwrap();
let mut writer = std::io::BufWriter::new(f);
let _bytes_io_count = std::io::copy(&mut reader, &mut writer).unwrap();
info!("Font file {} downloaded successfully!", font_path.display());
});
}
#[cfg(not(target_arch = "wasm32"))]
fn download_font(url: &str, folder: &str, filename: &str) {
use log::info;
let font_path = Path::new(folder).join(filename);
info!("Downloading font file {}...", font_path.display());
let request = ehttp::Request::get(url);
let response = ehttp::fetch_blocking(&request);
let mut reader = Cursor::new(response.unwrap().bytes);
let f = std::fs::File::create(&font_path).unwrap();
let mut writer = std::io::BufWriter::new(f);
let _bytes_io_count = std::io::copy(&mut reader, &mut writer).unwrap();
info!("Font file {} downloaded successfully!", font_path.display());
}
impl World for ShivaWorld {
fn book(&self) -> &Prehashed<FontBook> {
&self.book
}
fn library(&self) -> &Prehashed<Library> {
&self.library
}
fn main(&self) -> Source {
self.source.clone()
}
fn source(&self, _id: FileId) -> FileResult<Source> {
Ok(self.source.clone())
}
fn font(&self, id: usize) -> Option<Font> {
self.fonts.get(id).cloned()
}
fn file(&self, id: FileId) -> Result<typst::foundations::Bytes, FileError> {
let path = id.vpath();
let key = path.as_rootless_path().to_str().unwrap();
let img = self.img_map.get(key).unwrap();
Ok(img.clone())
}
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
let offset = offset.unwrap_or(0);
let offset = UtcOffset::from_hms(offset.try_into().ok()?, 0, 0).ok()?;
let time = OffsetDateTime::now_utc().checked_to_offset(offset)?;
Some(Datetime::Date(time.date()))
}
}
pub struct Transformer;
impl TransformerTrait for Transformer {
#[allow(unused)]
fn parse(document: &bytes::Bytes) -> anyhow::Result<Document> {
todo!()
}
fn generate(document: &Document) -> anyhow::Result<bytes::Bytes> {
let (text, _) = generate_document(document)?;
let bytes = Bytes::from(text);
Ok(bytes)
}
}
pub fn generate_document(
document: &Document,
) -> anyhow::Result<(TypstString, HashMap<String, typst::foundations::Bytes>)> {
fn process_header(source: &mut TypstString, level: usize, text: &str) -> anyhow::Result<()> {
let header_depth = "=".repeat(level);
let header_text = format!("{header_depth} {text}");
source.push_str(&header_text);
source.push('\n');
Ok(())
}
fn process_text(
source: &mut TypstString,
_size: u8,
text: &str,
is_bold: bool,
) -> anyhow::Result<()> {
if is_bold {
let bold_text = format!("*{text}*");
source.push_str(&bold_text);
} else {
source.push_str(text);
}
Ok(())
}
fn process_link(source: &mut TypstString, url: &str) -> anyhow::Result<()> {
let link = format!("#link(\"{url}\")");
source.push_str(&link);
Ok(())
}
fn process_table(
source: &mut TypstString,
headers: &Vec<TableHeader>,
rows: &Vec<TableRow>,
) -> anyhow::Result<()> {
let mut headers_text = TypstString::new();
for header in headers {
match &header.element {
Text { text, size } => {
headers_text.push('[');
process_text(&mut headers_text, *size, text, true)?;
headers_text.push(']');
headers_text.push(',');
}
_ => {
warn!(
"Should implement element for processing in inside table header - {:?}",
header.element
);
}
}
}
let mut cells_text = TypstString::new();
for row in rows {
for cell in &row.cells {
match &cell.element {
Text { text, size } => {
cells_text.push('[');
process_text(&mut cells_text, *size, text, false)?;
cells_text.push(']');
cells_text.push(',');
}
_ => {
warn!(
"Should implement element for processing in inside cell - {:?}",
cell.element
);
}
}
}
cells_text.push('\n');
}
let columns = headers.len();
let table_text = format!(
r#"
#table(
columns:{columns},
{headers_text}
{cells_text}
)
"#
);
source.push_str(&table_text);
Ok(())
}
fn process_list(
source: &mut TypstString,
img_map: &mut HashMap<String, typst::foundations::Bytes>,
list: &Vec<ListItem>,
numbered: bool,
depth: usize,
) -> anyhow::Result<()> {
source.push_str(&" ".repeat(depth));
for el in list {
if let List { elements, numbered } = &el.element {
process_list(source, img_map, elements, *numbered, depth + 1)?;
} else {
if numbered {
source.push_str("+ ")
} else {
source.push_str("- ")
};
process_element(source, img_map, &el.element)?;
}
}
Ok(())
}
fn process_image(
source: &mut TypstString,
bytes: &Bytes,
title: &str,
alt: &str,
image_type: &str,
) -> anyhow::Result<()> {
if !bytes.is_empty() {
let image_text = format!(
"
#image(\"{title}{image_type}\", alt: \"{alt}\")
"
);
source.push_str(&image_text);
}
Ok(())
}
fn process_element(
source: &mut TypstString,
img_map: &mut HashMap<String, typst::foundations::Bytes>,
element: &Element,
) -> anyhow::Result<()> {
match element {
Header { level, text } => process_header(source, *level as usize, text),
Paragraph { elements } => {
for paragraph_element in elements {
process_element(source, img_map, paragraph_element)?;
}
Ok(())
}
Text { text, size } => {
process_text(source, *size, text, false)?;
source.push('\n');
Ok(())
}
List { elements, numbered } => {
process_list(source, img_map, elements, *numbered, 0)?;
Ok(())
}
Hyperlink {
url,
title: _,
alt: _,
size: _,
} => {
process_link(source, url)?;
source.push('\n');
Ok(())
}
Table { headers, rows } => {
process_table(source, headers, rows)?;
Ok(())
}
Image(image) => {
let key = format!("{}{}", image.title(), image.image_type());
img_map.insert(key, typst::foundations::Bytes::from(image.bytes().to_vec()));
process_image(
source,
image.bytes(),
image.title(),
image.alt(),
&image.image_type().to_string(),
)?;
source.push('\n');
Ok(())
} }
}
let mut source = TypstString::new();
let mut img_map: HashMap<String, typst::foundations::Bytes> = HashMap::new();
let mut header_text = String::new();
document.get_page_header().iter().for_each(|el| match el {
Text { text, size: _ } => {
header_text.push_str(text);
}
_ => {}
});
let mut footer_text = String::new();
document.get_page_footer().iter().for_each(|el| match el {
Text { text, size: _ } => {
footer_text.push_str(text);
}
_ => {}
});
let footer_header_text = format!(
"#set page(
header: \"{header_text}\",
footer: \"{footer_text}\",
)\n"
);
source.push_str(&footer_header_text);
for element in &document.get_all_elements() {
process_element(&mut source, &mut img_map, element)?;
}
Ok((source, img_map))
}
#[cfg(test)]
mod test {
use crate::core::{disk_image_loader, TransformerWithImageLoaderSaverTrait};
use crate::markdown;
use bytes::Bytes;
use super::*;
#[test]
fn test_generate() -> anyhow::Result<()> {
let document = std::fs::read("test/data/document.md")?;
let documents_bytes = Bytes::from(document);
let parsed = markdown::Transformer::parse_with_loader(
&documents_bytes,
disk_image_loader("test/data"),
)?;
let generated_result = crate::typst::Transformer::generate(&parsed)?;
std::fs::write("test/data/document_from_md.typ", generated_result)?;
Ok(())
}
#[test]
fn test_generate_from_xml() -> anyhow::Result<()> {
let document = std::fs::read("test/data/document.xml")?;
let documents_bytes = Bytes::from(document);
let parsed = crate::xml::Transformer::parse(&documents_bytes)?;
let generated_result = crate::typst::Transformer::generate(&parsed)?;
std::fs::write("test/data/document_from_xml.typ", generated_result)?;
Ok(())
}
}