use std::{collections::HashMap, io::Write};
use typst::{
self, Library, LibraryExt,
diag::{FileError, FileResult},
foundations::{Bytes, Datetime},
layout::PagedDocument,
syntax::{FileId, Source, VirtualPath},
text::{Font, FontBook, FontInfo},
utils::LazyHash,
};
use crate::{
rich_string::{self, RichString},
screenplay::{DialogueElement, Element, Screenplay},
};
const TEMPLATE: &str = include_str!("template.typ");
pub fn export_typst(screenplay: &Screenplay, mut writer: impl Write, synopses: bool) {
let content = format_as_typst(screenplay, synopses);
write!(writer, "{content}").expect("Failed to write to typst document");
}
pub fn compile_document(screenplay: &Screenplay, synopses: bool) -> PagedDocument {
let (fontbook, fonts) = create_fontbook();
let content = format_as_typst(screenplay, synopses);
let worldplay = WorldPlay::new(content, &fontbook, &fonts);
typst::compile(&worldplay)
.output
.expect("Error compiling typst document")
}
fn format_as_typst(screenplay: &Screenplay, synopses: bool) -> String {
let formatted_elements = screenplay
.elements
.iter()
.map(|e| export_element(e, synopses))
.collect::<Vec<String>>();
let titlepage = export_titlepage(screenplay);
format!("{TEMPLATE}\n{titlepage}\n{}", formatted_elements.join("\n"))
}
fn export_titlepage(screenplay: &Screenplay) -> String {
if let Some(titlepage) = &screenplay.titlepage {
let title = format_titlepage_element(&titlepage.title);
let credit = format_titlepage_element(&titlepage.credit);
let authors = format_titlepage_element(&titlepage.authors);
let source = format_titlepage_element(&titlepage.source);
let draft_date = format_titlepage_element(&titlepage.draft_date);
let contact = format_titlepage_element(&titlepage.contact);
format!(
r#"#show: screenplay.with(
titlepage: true,
title: {title},
credit: {credit},
authors: {authors},
source: {source},
draft_date: {draft_date},
contact: {contact},
)"#
)
} else {
"#show: screenplay.with(titlepage: false)".to_string()
}
}
fn export_element(element: &Element, synopses: bool) -> String {
match element {
Element::Heading { slug, number } => {
if let Some(num) = number {
format!(
r#"#scene(number: "{}")[{}]"#,
replace_escaping(num),
format_rich_string(slug)
)
} else {
format!("#scene[{}]", format_rich_string(slug))
}
}
Element::Action(s) => format_rich_string(s),
Element::Dialogue(dialogue) => format!(
"#dialogue(paren: {})[{}][{}]",
format_character_extension(&dialogue.extension),
format_rich_string(&dialogue.character),
format_dialogue(&dialogue.elements),
),
Element::DualDialogue(dialogue1, dialogue2) => format!(
"#dual_dialogue(paren1: {}, paren2: {})[{}][{}][{}][{}]",
format_character_extension(&dialogue1.extension),
format_character_extension(&dialogue2.extension),
format_rich_string(&dialogue1.character),
format_dialogue(&dialogue1.elements),
format_rich_string(&dialogue2.character),
format_dialogue(&dialogue2.elements),
),
Element::Lyrics(s) => format!("#lyrics[{}]", format_rich_string(s)),
Element::Transition(s) => format!("#transition[{}]", format_rich_string(s)),
Element::CenteredText(s) => format!("#centered[{}]", format_rich_string(s)),
Element::Synopsis(s) => {
if synopses {
format!("#synopsis[{}]", format_rich_string(s))
} else {
"".to_string()
}
}
Element::PageBreak => "#pagebreak()".to_string(),
}
}
fn format_dialogue(dialogue: &[DialogueElement]) -> String {
dialogue
.iter()
.map(format_dialogue_element)
.collect::<Vec<String>>()
.join(" ")
}
fn format_character_extension(opt_ext: &Option<RichString>) -> String {
if let Some(ext) = opt_ext {
format!("[{}]", format_rich_string(ext))
} else {
"none".to_string()
}
}
fn format_dialogue_element(element: &DialogueElement) -> String {
match element {
DialogueElement::Parenthetical(s) => {
format!("#parenthetical[{}]", format_rich_string(s))
}
DialogueElement::Line(s) => format_rich_string(s),
}
}
fn format_rich_string(str: &RichString) -> String {
str.elements
.iter()
.map(format_rich_element)
.collect::<Vec<String>>()
.concat()
}
fn format_rich_element(element: &rich_string::Element) -> String {
if element.text == "\n" {
return "\\ ".to_string();
}
let mut out = format!(
"#text({}{}\"{}\")",
if element.is_bold() {
"weight: \"bold\","
} else {
""
},
if element.is_italic() {
"style: \"italic\","
} else {
""
},
replace_escaping(&element.text)
);
if element.is_underline() {
out = format!("#underline[{}]", out);
}
out
}
fn replace_escaping(s: &str) -> String {
s.replace("\\", "\\\\").replace("\"", "\\\"")
}
fn format_titlepage_element(element: &[RichString]) -> String {
if element.is_empty() {
return "none".to_string();
}
format!(
"[{}]",
element
.iter()
.map(format_rich_string)
.collect::<Vec<String>>()
.join("\\ ")
)
}
struct WorldPlay<'a> {
library: LazyHash<Library>,
book: &'a LazyHash<FontBook>,
source: HashMap<FileId, Source>,
fonts: &'a Vec<Font>,
}
const MAIN: &str = "/main.typ";
const FONTS: [&[u8]; 4] = [
include_bytes!("fonts/CourierPrime-Regular.ttf"),
include_bytes!("fonts/CourierPrime-Bold.ttf"),
include_bytes!("fonts/CourierPrime-Italic.ttf"),
include_bytes!("fonts/CourierPrime-BoldItalic.ttf"),
];
impl<'a> WorldPlay<'a> {
fn new(content: String, book: &'a LazyHash<FontBook>, fonts: &'a Vec<Font>) -> Self {
let mut sources = HashMap::with_capacity(1);
let main = create_source(MAIN, content);
let main_id = main.id();
sources.insert(main_id, main);
Self {
library: LazyHash::new(Library::default()),
book,
fonts,
source: sources,
}
}
}
impl typst::World for WorldPlay<'_> {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
self.book
}
fn main(&self) -> FileId {
create_file_id(MAIN)
}
fn source(&self, id: FileId) -> FileResult<Source> {
match self.source.get(&id) {
Some(s) => Ok(s.clone()),
None => FileResult::Err(FileError::NotSource),
}
}
fn file(&self, _: FileId) -> FileResult<Bytes> {
FileResult::Err(FileError::NotSource)
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.get(index).cloned()
}
fn today(&self, _: Option<i64>) -> Option<Datetime> {
None
}
}
fn create_source(filename: &str, content: String) -> Source {
let file_id = create_file_id(filename);
Source::new(file_id, content)
}
fn create_file_id(filename: &str) -> FileId {
FileId::new(None, VirtualPath::new(filename))
}
fn create_fontbook() -> (LazyHash<FontBook>, Vec<Font>) {
let mut fonts = Vec::new();
let mut fontbook = FontBook::new();
for font_data in FONTS.iter() {
let font = match Font::new(Bytes::new(font_data), 0) {
Some(f) => f,
None => continue,
};
fonts.push(font);
let info = FontInfo::new(font_data, 0).expect("Could not parse font");
fontbook.push(info);
}
(LazyHash::new(fontbook), fonts)
}