use std::io::Write;
use crate::{
export::Exporter,
rich_string::{self, RichString},
screenplay::{Dialogue, DialogueElement, Element, Screenplay, Span, TitlePage},
};
const CSS: &str = include_str!("style.css");
#[derive(Default)]
pub struct HtmlExporter {
pub standalone: bool,
pub synopses: bool,
pub include_source_positions: bool,
}
impl Exporter for HtmlExporter {
fn file_extension(&self) -> &'static str {
"html"
}
fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> std::io::Result<()> {
if self.standalone {
writeln!(writer, r"<!DOCTYPE html><html>{}", Self::export_head())?;
}
writeln!(writer, r#"<body><div id="wrapper" class="screenplay">"#)?;
if let Some(titlepage) = &screenplay.titlepage {
writeln!(writer, "{}", self.export_titlepage(titlepage))?;
}
for e in &screenplay.elements {
writeln!(writer, "{}", self.export_element(e))?;
}
writeln!(writer, "</div></body>")?;
if self.standalone {
writeln!(writer, "</html>")?;
}
Ok(())
}
}
impl HtmlExporter {
pub fn export_head() -> String {
format!(
r#"<head>
<title>Screenplay</title>
<style type="text/css">{CSS}</style></head>"#,
)
}
fn export_titlepage(&self, titlepage: &TitlePage) -> String {
format!(
r#"
<div id="title-page">
{}
{}
{}
{}
{}
{}
</div>"#,
self.export_titlepage_element("title", &titlepage.title),
self.export_titlepage_element("credit", &titlepage.credit),
self.export_titlepage_element("authors", &titlepage.authors),
self.export_titlepage_element("source", &titlepage.source),
self.export_titlepage_element("draft-date", &titlepage.draft_date),
self.export_titlepage_element("contact", &titlepage.contact),
)
}
fn export_titlepage_element(&self, value: &str, element: &[RichString]) -> String {
if element.is_empty() {
return String::new();
}
let content = element
.iter()
.map(|s| format!("<p>{}</p>", self.format_rich_string(s)))
.collect::<Vec<String>>()
.concat();
format!(r#"<div class="{value}">{content}</div>"#)
}
fn export_element(&self, element: &Span<Element>) -> String {
if !self.synopses && matches!(element.inner, Element::Synopsis(_)) {
return String::new();
}
let (class, content) = match &element.inner {
Element::Heading { slug, number } => (
"scene-heading",
format!(
"{}{}{}",
if let Some(x) = number {
format!(r#"<span class="scnuml">{x}</span>"#)
} else {
String::new()
},
self.format_rich_string(slug),
if let Some(x) = number {
format!(r#"<span class="scnumr">{x}</span>"#)
} else {
String::new()
},
),
),
Element::Action(s) => ("action", format!("<p>{}</p>", self.format_rich_string(s))),
Element::Dialogue(dialogue) => (
"dialogue",
format!(
r#"<p class="character">{}</p>{}"#,
self.format_character(dialogue),
self.format_dialogue(&dialogue.elements),
),
),
Element::DualDialogue(dialogue1, dialogue2) => (
"dual",
format!(
r#"
<div class="left">
<p class="character">{}</p>
{}
</div>
<div class="right">
<p class="character">{}</p>
{}
</div>"#,
self.format_character(dialogue1),
self.format_dialogue(&dialogue1.elements),
self.format_character(dialogue2),
self.format_dialogue(&dialogue2.elements),
),
),
Element::Lyrics(s) => (
"lyrics",
format!(r#"<p>{}</p>"#, self.format_rich_string(s)),
),
Element::Transition(s) => ("transition", self.format_rich_string(s)),
Element::CenteredText(s) => (
"action centered",
format!(r"<p>{}</p>", self.format_rich_string(s)),
),
Element::Synopsis(s) => (
"synopsis",
format!(r#"<p>{}</p>"#, self.format_rich_string(s)),
),
Element::PageBreak => ("", String::new()), };
format!(
r#"<div class="{}" {}>{}</div>"#,
class,
if self.include_source_positions {
format!(
r#"data-start-line="{}" data-end-line="{}""#,
element.start_line, element.end_line
)
} else {
String::new()
},
content
)
}
fn format_character(&self, dialogue: &Dialogue) -> String {
if let Some(extension) = &dialogue.extension {
format!(
"{} ({})",
self.format_rich_string(&dialogue.character),
self.format_rich_string(extension)
)
} else {
self.format_rich_string(&dialogue.character)
}
}
fn format_rich_string(&self, str: &RichString) -> String {
str.elements
.iter()
.map(|e| self.format_rich_element(e))
.collect::<Vec<String>>()
.concat()
}
fn format_rich_element(&self, element: &rich_string::Element) -> String {
if element.text == "\n" {
return "<br />".to_string();
}
let prepend = format!(
"{}{}{}",
if element.is_bold() { "<strong>" } else { "" },
if element.is_italic() { "<em>" } else { "" },
if element.is_underline() { "<u>" } else { "" },
);
let append = format!(
"{}{}{}",
if element.is_underline() { "</u>" } else { "" },
if element.is_italic() { "</em>" } else { "" },
if element.is_bold() { "</strong>" } else { "" },
);
format!("{prepend}{}{append}", element.text)
}
fn format_dialogue(&self, dialogue: &[DialogueElement]) -> String {
dialogue
.iter()
.map(|d| self.format_dialogue_element(d))
.collect::<Vec<String>>()
.join("\n")
}
fn format_dialogue_element(&self, element: &DialogueElement) -> String {
match element {
DialogueElement::Parenthetical(s) => {
format!(
r#"<p class="parenthetical">{}</p>"#,
self.format_rich_string(s)
)
}
DialogueElement::Line(s) => format!(r"<p>{}</p>", self.format_rich_string(s)),
}
}
}