jumpcut 0.8.1

JumpCut is a library and CLI for converting Fountain-formatted text files into FDX and HTML formats.
Documentation
use super::shared::{escape_html, join_metadata, sorted_style_names};
use crate::{Attributes, Element, ElementText, Screenplay};
use std::fmt::Write;

const HTML_STYLE: &str = include_str!("../templates/html_style.css");

pub(crate) fn render_document(screenplay: &Screenplay, head: bool) -> String {
    let mut out = String::with_capacity(32 * 1024);
    if head {
        out.push_str("<!doctype html>\n\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n  <title>");
        out.push_str(&escape_html(&join_metadata(&screenplay.metadata, "title", " ")));
        out.push_str("</title>\n\n  <style type=\"text/css\" media=\"screen\">\n   ");
        out.push_str(HTML_STYLE);
        out.push_str("\n  </style>\n</head>\n\n<body>\n");
    }

    out.push_str("<section class=\"screenplay\">\n");
    render_body(&mut out, screenplay);
    out.push_str("</section>\n");

    if head {
        out.push_str("</body>\n</html>\n");
    }

    out
}

fn render_body(out: &mut String, screenplay: &Screenplay) {
    if screenplay.metadata.get("title").is_some() {
        out.push_str("    <section class=\"title-page\">\n        <div class=\"title\">\n            <h1>");
        out.push_str(&escape_html(&join_metadata(&screenplay.metadata, "title", "")));
        out.push_str("</h1>\n            <p>");
        out.push_str(&escape_html(&join_metadata(&screenplay.metadata, "credit", "")));
        out.push_str("</p>\n            <p>");
        out.push_str(&escape_html(&join_metadata(&screenplay.metadata, "author", "")));
        out.push_str(&escape_html(&join_metadata(&screenplay.metadata, "authors", "")));
        out.push_str("</p>\n        </div>\n    </section>\n");
    }

    out.push_str("        <section class=\"body\">\n");
    for element in &screenplay.elements {
        match element {
            Element::DialogueBlock(block) => {
                out.push_str("    <div class=\"dialogueBlock\">\n");
                for child in block {
                    render_paragraph(out, child);
                }
                out.push_str("                </div>\n");
            }
            Element::DualDialogueBlock(blocks) => {
                out.push_str("                <div class=\"dualDialogueBlock\">\n");
                for block in blocks {
                    out.push_str("                    <div class=\"dialogueBlock\">\n");
                    if let Element::DialogueBlock(dialogue_block) = block {
                        for child in dialogue_block {
                            render_paragraph(out, child);
                        }
                    }
                    out.push_str("                    </div>\n");
                }
                out.push_str("                </div>\n");
            }
            _ => render_paragraph(out, element),
        }
    }
    out.push_str("        </section>\n");
}

fn render_paragraph(out: &mut String, element: &Element) {
    let (type_name, text, attributes) = match element {
        Element::Action(text, attributes)
        | Element::Character(text, attributes)
        | Element::SceneHeading(text, attributes)
        | Element::Lyric(text, attributes)
        | Element::Parenthetical(text, attributes)
        | Element::Dialogue(text, attributes)
        | Element::Transition(text, attributes)
        | Element::ColdOpening(text, attributes)
        | Element::NewAct(text, attributes)
        | Element::EndOfAct(text, attributes) => (element.name(), text, attributes),
        Element::Section(text, attributes, _) => ("Section", text, attributes),
        Element::Synopsis(text) => ("Synopsis", text, &Attributes::default()),
        Element::DialogueBlock(_) | Element::DualDialogueBlock(_) | Element::PageBreak => return,
    };

    write!(
        out,
        "                <p class=\"{}{}{}\">",
        class_name(type_name),
        if attributes.starts_new_page {
            " startsNewPage"
        } else {
            ""
        },
        if attributes.centered { " centered" } else { "" }
    )
    .unwrap();
    render_text(out, text);
    out.push_str("</p>\n");
}

fn render_text(out: &mut String, text: &ElementText) {
    match text {
        ElementText::Plain(text) => out.push_str(&escape_html(text)),
        ElementText::Styled(runs) => {
            for run in runs {
                let classes = sorted_style_names(run, false);
                if classes.is_empty() {
                    out.push_str(&escape_html(&run.content));
                } else {
                    write!(out, "<span class=\"{}\">", classes.join(" ")).unwrap();
                    out.push_str(&escape_html(&run.content));
                    out.push_str("</span>");
                }
            }
        }
    }
}

fn class_name(type_name: &str) -> &'static str {
    match type_name {
        "Scene Heading" => "sceneHeading",
        "Action" => "action",
        "Character" => "character",
        "Dialogue" => "dialogue",
        "Parenthetical" => "parenthetical",
        "Transition" => "transition",
        "Lyric" => "lyric",
        "Section" => "section",
        "Synopsis" => "synopsis",
        "Cold Opening" => "coldOpening",
        "New Act" => "newAct",
        "End of Act" => "endOfAct",
        _ => "unknown",
    }
}