use crate::Screenplay;
use serde_json;
impl Screenplay {
#[cfg(feature = "fdx")]
pub fn to_final_draft(&mut self) -> String {
crate::rendering::fdx::prepare_screenplay(self);
crate::rendering::fdx::render_document(self)
}
#[cfg(feature = "html")]
pub fn to_html(&mut self, head: bool) -> String {
crate::rendering::html::render_document(self, head)
}
pub fn to_json_string(self) -> String {
serde_json::to_string(&self)
.expect("Should be impossible for this JSON serialization to fail.")
}
pub fn to_json_value(self) -> serde_json::Value {
serde_json::to_value(&self)
.expect("Should be impossible for this JSON serialization to fail.")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rendering::fdx::{add_fdx_formatting, insert_metadata_value};
use crate::{blank_attributes, p, tr, Element, ElementText, Metadata};
use handlebars::{Handlebars, JsonRender};
use pretty_assertions::assert_eq;
use std::collections::HashMap;
#[test]
fn test_add_fdx_formatting() {
let mut metadata: Metadata = HashMap::new();
let mut expected: Metadata = HashMap::new();
let defaults: Vec<(&str, &str)> = vec![
("scene-heading-style", "AllCaps"),
("space-before-heading", "24"),
("dialogue-spacing", "1"),
("action-text-style", ""),
("font-choice", "Courier Prime"),
("dialogue-left-indent", "2.50"),
("dialogue-right-indent", "6.00"),
];
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
add_fdx_formatting(&mut metadata);
assert_eq!(metadata, expected, "it should produce the correct defaults");
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "bsh ush");
insert_metadata_value(
&mut expected,
"scene-heading-style",
"AllCaps+Bold+Underline",
);
insert_metadata_value(&mut expected, "fmt", "bsh ush");
add_fdx_formatting(&mut metadata);
assert_eq!(metadata, expected, "it should handle scene-heading-style");
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "acat");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "action-text-style", "AllCaps");
insert_metadata_value(&mut expected, "fmt", "acat");
add_fdx_formatting(&mut metadata);
assert_eq!(metadata, expected, "it should handle action-text-style");
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "dsd");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "dialogue-spacing", "2");
insert_metadata_value(&mut expected, "fmt", "dsd");
add_fdx_formatting(&mut metadata);
assert_eq!(metadata, expected, "it should handle dialogue-spacing");
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "cfd");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "font-choice", "Courier Final Draft");
insert_metadata_value(&mut expected, "fmt", "cfd");
add_fdx_formatting(&mut metadata);
assert_eq!(metadata, expected, "it should handle font-choice");
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "dl-1.25");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "dialogue-left-indent", "1.25");
insert_metadata_value(&mut expected, "fmt", "dl-1.25");
add_fdx_formatting(&mut metadata);
assert_eq!(metadata, expected, "it should handle dialogue-left-indent");
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "dr-4.75");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "dialogue-right-indent", "4.75");
insert_metadata_value(&mut expected, "fmt", "dr-4.75");
add_fdx_formatting(&mut metadata);
assert_eq!(metadata, expected, "it should handle dialogue-right-indent");
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "dl-3.0 dr-5.5");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "dialogue-left-indent", "3.0");
insert_metadata_value(&mut expected, "dialogue-right-indent", "5.5");
insert_metadata_value(&mut expected, "fmt", "dl-3.0 dr-5.5");
add_fdx_formatting(&mut metadata);
assert_eq!(
metadata, expected,
"it should handle both dialogue indents together"
);
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "dl-2 dr-8 bsh acat");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "dialogue-left-indent", "2");
insert_metadata_value(&mut expected, "dialogue-right-indent", "8");
insert_metadata_value(&mut expected, "scene-heading-style", "AllCaps+Bold");
insert_metadata_value(&mut expected, "action-text-style", "AllCaps");
insert_metadata_value(&mut expected, "fmt", "dl-2 dr-8 bsh acat");
add_fdx_formatting(&mut metadata);
assert_eq!(
metadata, expected,
"it should handle dialogue indents combined with other options"
);
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "dl-invalid");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "fmt", "dl-invalid");
add_fdx_formatting(&mut metadata);
assert_eq!(
metadata, expected,
"it should ignore invalid dialogue-left-indent values"
);
metadata = HashMap::new();
insert_metadata_value(&mut metadata, "fmt", "dr-notanumber");
for pair in defaults.iter() {
insert_metadata_value(&mut expected, pair.0, pair.1);
}
insert_metadata_value(&mut expected, "fmt", "dr-notanumber");
add_fdx_formatting(&mut metadata);
assert_eq!(
metadata, expected,
"it should ignore invalid dialogue-right-indent values"
);
}
#[test]
fn test_html_renderer_matches_template_output() {
let mut screenplay = sample_screenplay();
let actual = normalize_markup(&screenplay.to_html(true));
let expected = normalize_markup(&render_html_with_handlebars(&sample_screenplay(), true));
assert_eq!(actual, expected);
}
#[test]
fn test_fdx_renderer_matches_template_output() {
let mut screenplay = sample_screenplay();
let actual = screenplay.to_final_draft();
assert!(actual.contains("<FinalDraft DocumentType=\"Script\" Template=\"No\" Version=\"4\">"));
assert!(actual.contains("<Paragraph Type=\"Scene Heading\" Number=\"1\">"));
assert!(actual.contains("<Text Style=\"Bold\">BOLD</Text>"));
assert!(actual.contains("<DualDialogue>"));
assert!(actual.contains("<ElementSettings Type=\"Dialogue\">"));
assert!(actual.contains("LeftIndent=\"2.50\" RightIndent=\"6.00\" SpaceBefore=\"0\" Spacing=\"1\""));
assert!(actual.contains("<Text AdornmentStyle=\"-1\""));
assert!(actual.contains("Font=\"Courier Prime\""));
assert!(actual.contains(">DRAFT</Text>"));
assert!(actual.contains(">DATE</Text>"));
}
fn sample_screenplay() -> Screenplay {
let mut metadata = Metadata::new();
metadata.insert("title".into(), vec!["TITLE".into()]);
metadata.insert("credit".into(), vec!["BY".into()]);
metadata.insert("author".into(), vec!["AUTHOR".into()]);
metadata.insert("source".into(), vec!["SOURCE".into()]);
metadata.insert("draft".into(), vec!["DRAFT".into()]);
metadata.insert("draft date".into(), vec!["DATE".into()]);
metadata.insert("fmt".into(), vec!["bsh".into()]);
let styled = ElementText::Styled(vec![tr("BOLD", vec!["Bold"]), tr("ITALIC", vec!["Italic"])]);
let mut scene_attrs = blank_attributes();
scene_attrs.scene_number = Some("1".into());
let mut centered_attrs = blank_attributes();
centered_attrs.centered = true;
Screenplay {
metadata,
elements: vec![
Element::SceneHeading(p("INT.KITCHEN"), scene_attrs),
Element::Action(styled, blank_attributes()),
Element::DialogueBlock(vec![
Element::Character(p("ALICE"), blank_attributes()),
Element::Dialogue(p("HELLO"), blank_attributes()),
]),
Element::DualDialogueBlock(vec![
Element::DialogueBlock(vec![
Element::Character(p("BOB"), blank_attributes()),
Element::Dialogue(p("ONE"), blank_attributes()),
]),
Element::DialogueBlock(vec![
Element::Character(p("CAROL"), blank_attributes()),
Element::Dialogue(p("TWO"), blank_attributes()),
]),
]),
Element::ColdOpening(p("COLD"), centered_attrs),
],
}
}
fn normalize_markup(input: &str) -> String {
input.chars().filter(|c| !c.is_whitespace()).collect()
}
fn render_html_with_handlebars(screenplay: &Screenplay, head: bool) -> String {
let template = if head {
include_str!("templates/html.hbs")
} else {
include_str!("templates/body.hbs")
};
let mut handlebars = Handlebars::new();
handlebars.register_helper(
"type_to_class",
Box::new(
|h: &handlebars::Helper<'_>,
_: &Handlebars<'_>,
_: &handlebars::Context,
_: &mut handlebars::RenderContext<'_, '_>,
out: &mut dyn handlebars::Output|
-> Result<(), handlebars::RenderError> {
let output = match h.param(0).map(|p| p.value().render()) {
Some(value) if value == "Scene Heading" => "sceneHeading",
Some(value) if value == "Action" => "action",
Some(value) if value == "Character" => "character",
Some(value) if value == "Dialogue" => "dialogue",
Some(value) if value == "Parenthetical" => "parenthetical",
Some(value) if value == "Transition" => "transition",
Some(value) if value == "Lyric" => "lyric",
Some(value) if value == "Section" => "section",
Some(value) if value == "Synopsis" => "synopsis",
Some(value) if value == "Cold Opening" => "coldOpening",
Some(value) if value == "New Act" => "newAct",
Some(value) if value == "End of Act" => "endOfAct",
_ => "unknown",
};
out.write(output)?;
Ok(())
},
),
);
handlebars.register_helper(
"style",
Box::new(
|h: &handlebars::Helper<'_>,
_: &Handlebars<'_>,
_: &handlebars::Context,
_: &mut handlebars::RenderContext<'_, '_>,
out: &mut dyn handlebars::Output|
-> Result<(), handlebars::RenderError> {
let value = h.param(0).map(|p| p.value().render()).unwrap_or_default();
out.write(&value.to_lowercase())?;
Ok(())
},
),
);
handlebars
.register_template_string("html", template)
.expect("Expect template to load.");
handlebars.render("html", screenplay).unwrap()
}
}