### Create RichString and inspect elements Source: https://docs.rs/rustwell/latest/rustwell/rich_string/struct.RichString.html Shows how to create a RichString from a string with bold styling and inspect its elements. The example asserts the text and bold style of the parsed elements. ```rust use rustwell::rich_string::RichString; let mut rs = RichString::from("Hello **world!**"); assert_eq!(rs.elements[0].text, "Hello ".to_string()); assert_eq!(rs.elements[1].text, "world!".to_string()); assert!(rs.elements[1].is_bold()); ``` -------------------------------- ### Test module for preprocessor Source: https://docs.rs/rustwell/latest/src/rustwell/parser.rs.html Starts the test module for the preprocessor. It imports the parent module's items and defines a nested module for preprocessor-specific tests. ```rust 674#[cfg(test)] 675mod tests { 676 use super::*; 677 678 mod preprocessor { 679 use super::*; 680 ``` -------------------------------- ### Example: Parsing a Fountain string with rustwell::parse Source: https://docs.rs/rustwell/latest/rustwell/fn.parse.html Demonstrates using `rustwell::parse` to parse a Fountain source string. The example creates a simple screenplay with a title and a scene, then asserts that the resulting `Screenplay` has at least one element. ```rust use rustwell::parse; let input = r#" Title: Example Screenplay INT. ROOM – DAY A man stands alone. "#; let screenplay = parse(input); assert!(screenplay.elements.len() > 0); ``` -------------------------------- ### Implement Preprocessor constructor and append_and_advance Source: https://docs.rs/rustwell/latest/src/rustwell/parser.rs.html Implements the Preprocessor constructor and the append_and_advance method. The constructor initializes the result vector, current line with line number 1, source line counter, note state to None, and stores the source string. append_and_advance splits the input on newlines, appends the first segment to the current line, and for each subsequent segment pushes the current line to either the note buffer or the result, increments the source line, and starts a new current line with that segment. ```rust impl<'a> Preprocessor<'a> { fn new(src: &'a str) -> Self { Preprocessor { result: Vec::new(), current_line: (1, String::new()), source_line: 1, note_state: None, rest: src, } } ``` ```rust /// Appends the given [`&str`] to the `current_line`. /// When encountering newlines it appends the `current_line` to `result` /// and advances `source_line`. fn append_and_advance(&mut self, s: &str) { let mut lines = s.split('\n'); if let Some(first) = lines.next() { self.current_line.1.push_str(first); } for segment in lines { if let Some(NoteState { buffer, .. }) = &mut self.note_state { buffer.push(std::mem::take(&mut self.current_line)); } else { self.result.push(std::mem::take(&mut self.current_line)); } self.source_line += 1; self.current_line.0 = self.source_line; self.current_line.1.push_str(segment); } } ``` -------------------------------- ### Get element from global index Source: https://docs.rs/rustwell/latest/rustwell/rich_string/struct.RichString.html Shows how to use get_element_from_index to find the element containing a global index and the local index within that element. The example uses pattern matching to verify the returned tuple. ```rust use rustwell::rich_string::RichString; let mut rs = RichString::from("He**ll**o"); assert!(matches!(rs.get_element_from_index(1), Some((_, 1)))); assert!(matches!(rs.get_element_from_index(2), Some((_, 0)))); assert!(matches!(rs.get_element_from_index(3), Some((_, 1)))); assert!(matches!(rs.get_element_from_index(4), Some((_, 0)))); ``` -------------------------------- ### Convert RichString to plain string Source: https://docs.rs/rustwell/latest/rustwell/rich_string/struct.RichString.html Demonstrates converting a RichString to a plain string without style delimiters using to_plain_string. The example asserts the resulting plain string. ```rust use rustwell::rich_string::RichString; let mut rs = RichString::from("He**ll**o"); assert_eq!(rs.to_plain_string(), "Hello".to_string()); ``` -------------------------------- ### Get character by global index Source: https://docs.rs/rustwell/latest/rustwell/rich_string/struct.RichString.html Demonstrates the get_char method, which retrieves a character by a global index ignoring style attributes. The example shows expected results for valid and out-of-range indices. ```rust use rustwell::rich_string::RichString; let mut rs = RichString::from("He**llo**"); assert_eq!(rs.get_char(1), Some('e')); assert_eq!(rs.get_char(3), Some('l')); assert_eq!(rs.get_char(5), None); ``` -------------------------------- ### Test nested bold in italics with test_emphasis! macro Source: https://docs.rs/rustwell/latest/src/rustwell/rich_string.rs.html Starts a test for nested emphasis where bold appears inside italics. The input is "*foo **bar** baz*" and the expected output is not shown in the source. ```rust test_emphasis!( nested_bold_in_italics, "*foo **bar** baz*", ``` -------------------------------- ### Parse and export a Fountain script to HTML with rustwell Source: https://docs.rs/rustwell/latest/rustwell/index.html Parses a Fountain screenplay string and exports it to HTML using the HtmlExporter. The example defines a script with a title, scene heading, action, character, and parenthetical, then calls parse and export_to_stdout. Requires the rustwell crate and the ExporterExt trait for the export method. ```rust use rustwell::{Exporter, ExporterExt, HtmlExporter}; use rustwell::parse; let script = r#" Title: Example Screenplay INT. HOUSE – DAY Someone is knocking on the door. GUEST (tired) Hey, open up! "#; let parsed = parse(script); let exporter = HtmlExporter { ..Default::default() }; exporter.export_to_stdout(&parsed).unwrap() ``` -------------------------------- ### Test break_points with RichString examples Source: https://docs.rs/rustwell/latest/src/rustwell/export/pdf.rs.html Unit tests for the break_points function, covering simple words, explicit newlines, long words requiring a break inside a word, hyphenated words, and rich text with formatting markers. Each test constructs a RichString, calls break_points with a span, and asserts the resulting BreakPoint vector matches the expected index and break type. ```rust #[test] fn breaks_simple() { let mut rs = RichString::new(); rs.push_str("hello world"); let breakpoints = break_points(&rs, 6); let correct = vec![BreakPoint { index: 6, break_type: BreakType::NewLine, }]; assert_eq!(breakpoints, correct); } ``` ```rust #[test] fn breaks_simple_with_newline() { let mut rs = RichString::new(); rs.push_str("hello\nworld"); let breakpoints = break_points(&rs, 100); let correct = vec![BreakPoint { index: 5, break_type: BreakType::NewLine, }]; assert_eq!(breakpoints, correct); } ``` ```rust #[test] fn breaks_simple_breakword() { let mut rs = RichString::new(); rs.push_str("helloworld"); let breakpoints = break_points(&rs, 6); let correct = vec![BreakPoint { index: 5, break_type: BreakType::BreakWord, }]; assert_eq!(breakpoints, correct); } ``` ```rust #[test] fn breaks_simple_utilizing_hyphen() { let mut rs = RichString::new(); rs.push_str("hello-world"); let breakpoints = break_points(&rs, 7); let correct = vec![BreakPoint { index: 6, break_type: BreakType::NewLine, }]; assert_eq!(breakpoints, correct); } ``` ```rust #[test] fn breaks_rich() { let mut rs = RichString::new(); rs.push_str("he**ll**o wor*ld*"); let breakpoints = break_points(&rs, 6); let correct = vec![BreakPoint { index: 6, break_type: BreakType::NewLine, }]; assert_eq!(breakpoints, correct); } ``` -------------------------------- ### Collect indented block lines Source: https://docs.rs/rustwell/latest/src/rustwell/parser.rs.html Collects consecutive lines that start with three spaces into a vector of trimmed RichString values. Stops at the first line that does not start with three spaces. ```rust fn take_indented_block(&mut self) -> Vec { let mut out = Vec::new(); while let Some((_, next)) = self.lines.peek().copied() { if next.starts_with(" ") { self.lines.next(); out.push(RichString::from(next.trim())); } else { break; } } out } ``` -------------------------------- ### Create and inspect a RichString Source: https://docs.rs/rustwell/latest/rustwell/rich_string/index.html Demonstrates how to convert a string with Fountain/CommonMark syntax into a RichString and verify the resulting styled elements. ```rust use rustwell::rich_string::RichString; let rs: RichString = "_Hello_ **world!**".into(); assert_eq!(rs.elements[0].text, "Hello".to_string()); assert!(rs.elements[0].is_underline()); assert_eq!(rs.elements[1].text, " ".to_string()); assert_eq!(rs.elements[2].text, "world!".to_string()); assert!(rs.elements[2].is_bold()); ``` -------------------------------- ### RichString::get_char Source: https://docs.rs/rustwell/latest/rustwell/rich_string/struct.RichString.html Gets a character from a global index, ignoring style attributes. ```APIDOC ## RichString::get_char ### Description Gets a `char` from a "global" index, meaning the index when viewing the `RichString` as a single string without any style attributes taken into account. ### Method `pub fn get_char(&self, index: usize) -> Option` ### Parameters - `index` (usize) - Required - The global index of the character to retrieve. ### Returns `Option` - The character at the given index, or `None` if the index is out of bounds. ### Examples ```rust use rustwell::rich_string::RichString; let mut rs = RichString::from("He**llo**"); assert_eq!(rs.get_char(1), Some('e')); assert_eq!(rs.get_char(3), Some('l')); assert_eq!(rs.get_char(5), None); ``` ``` -------------------------------- ### Parser methods for section, synopsis, page break, forced action, centered text, lyrics, action, and heading Source: https://docs.rs/rustwell/latest/src/rustwell/parser.rs.html These methods are part of a parser that recognizes different line types in a markup language. Each method uses a common `try_` helper to match a prefix and then either append to an existing element or create a new one, updating the parser state. The `try_heading` method is incomplete in the source, so it is included as-is. ```rust fn try_section(&mut self, line: &str) -> bool { self.try_( line, |_, s| s.trim_start().starts_with('#').then_some(s), |_, _| {}, ) } ``` ```rust fn try_synopsis(&mut self, line: &str, line_idx: usize) -> bool { self.try_( line, |_, s| s.trim_start().strip_prefix('='), |this, inner| { if this.state == State::InBlock && let Some(Span { start_line: _, end_line, inner: Element::Synopsis(rs), }) = this.elements.last_mut() { rs.push_str("\n"); rs.push_str(inner); *end_line = line_idx; return; } let rs = RichString::from(inner); this.elements .push(Span::new(Element::Synopsis(rs), line_idx)); this.state = State::InBlock; }, ) } ``` ```rust fn try_page_break(&mut self, line: &str, line_idx: usize) -> bool { self.try_( line, |_, s| s.trim_start().starts_with("===").then_some(s), |this, _| this.elements.push(Span::new(Element::PageBreak, line_idx)), ) } ``` ```rust fn try_forced_action(&mut self, line: &str, line_idx: usize) -> bool { self.try_( line, |_, s| s.trim_start().strip_prefix('!'), |this, inner| { this.elements.push(Span::new( Element::Action(RichString::from(inner)), line_idx, )); this.state = State::InBlock; }, ) } ``` ```rust fn try_centered(&mut self, line: &str, line_idx: usize) -> bool { self.try_( line, |_, s| s.trim().strip_prefix('>').and_then(|u| u.strip_suffix('<')), |this, inner| { let inner = inner.trim(); if this.state == State::InBlock && let Some(Span { start_line: _, end_line, inner: Element::CenteredText(rs), }) = this.elements.last_mut() { rs.push_str("\n"); rs.push_str(inner); *end_line = line_idx; return; } let rs = RichString::from(inner); this.elements .push(Span::new(Element::CenteredText(rs), line_idx)); this.state = State::InBlock; }, ) } ``` ```rust fn try_lyrics(&mut self, line: &str, line_idx: usize) -> bool { self.try_( line, |_, s| s.trim_start().strip_prefix('~'), |this, inner| { if this.state == State::InBlock && let Some(Span { start_line: _, end_line, inner: Element::Lyrics(rs), }) = this.elements.last_mut() { rs.push_str("\n"); rs.push_str(inner); *end_line = line_idx; return; } let rs = RichString::from(inner); this.elements.push(Span::new(Element::Lyrics(rs), line_idx)); this.state = State::InBlock; }, ) } ``` ```rust fn try_action(&mut self, line: &str, line_idx: usize) -> bool { self.try_( line, |_, line| Some(line), |this, inner| { if this.state == State::InBlock && let Some(Span { start_line: _, end_line, inner: Element::Action(rs), }) = this.elements.last_mut() { rs.push_str("\n"); rs.push_str(inner); *end_line = line_idx; return; } let rs = RichString::from(inner); this.elements.push(Span::new(Element::Action(rs), line_idx)); this.state = State::InBlock; }, ) } ``` ```rust fn try_heading(&mut self, line: &str, line_idx: usize) -> bool { self.try_( line, |this, line| { let trimmed = line.trim_start(); ``` -------------------------------- ### TitlePage::new Source: https://docs.rs/rustwell/latest/rustwell/screenplay/struct.TitlePage.html Constructs a new empty TitlePage instance. ```APIDOC ## TitlePage::new ### Description Creates a new empty `TitlePage` instance. This constructor initializes all fields as empty vectors, as none are strictly required according to the fountain specification. ### Method `pub fn new() -> Self` ### Parameters None. ### Returns - `Self` - A new `TitlePage` with all fields set to empty vectors. ### Example ```rust let title_page = TitlePage::new(); ``` ``` -------------------------------- ### RichString::new Source: https://docs.rs/rustwell/latest/rustwell/rich_string/struct.RichString.html Creates a new, empty, `RichString`. ```APIDOC ## RichString::new ### Description Creates a new, empty, `RichString`. ### Method `pub fn new() -> Self` ### Parameters None. ### Returns A new, empty `RichString`. ``` -------------------------------- ### Element Layout and Pagination Loop Source: https://docs.rs/rustwell/latest/src/rustwell/export/pdf.rs.html Processes elements for PDF layout, handling headings with numbers, actions, and dialogues. Uses a macro to call write_element with common parameters. Manages pagination via line_idx and max_lines_per_page, and preserves residual breakpoint index for dialogues. ```rust start_line: _, end_line: _, inner: element, }) = element_iter.peek() { if line_idx >= max_lines_per_page { break; } let mut breakpoint_idx = match residual_breakpoint_idx { Some(i) => { // If we're in a dialogue element, we need to preserve // `residual_breakpoint_idx`. if !matches!(element, Element::Dialogue(_)) { residual_breakpoint_idx = None; } i } None => 0, }; /// Macro for the most common usage of write_element(...), as most types of /// [`Element`]s call this function with almost identical parameters. macro_rules! write_element { ($content:expr, $margin:expr, $text_direction:expr) => { residual_breakpoint_idx = write_element( layout_info, $content, $margin, &mut breakpoint_idx, &mut line_idx, max_lines_per_page, &mut surface, $text_direction, )? }; } match &element { Element::Heading { slug, number } => { if number.is_some() { let initial_line_index = line_idx; let left_number_margin = Margin { left: 54.0, right: layout_info.size.x as f32 - MARGINS.heading.left + 18.0, }; let right_number_margin = Margin { left: layout_info.size.x as f32 - MARGINS.heading.right + 18.0, right: 18.0, }; let rich_number = &number.as_ref().unwrap().into(); write_element( layout_info, rich_number, &left_number_margin, &mut 0, &mut initial_line_index.clone(), max_lines_per_page, &mut surface, Alignment::LeftToRight, )?; write_element( layout_info, rich_number, &right_number_margin, &mut 0, &mut initial_line_index.clone(), max_lines_per_page, &mut surface, Alignment::RightToLeft, )?; } outline.push_child(OutlineNode::new( slug.to_plain_string(), XyzDestination::new( page_idx, Point { x: MARGINS.heading.left, y: (TOP_MARGIN + (line_idx * FONT_SIZE) - FONT_SIZE) as f32, }, ), )); write_element!(slug, &MARGINS.heading, Alignment::LeftToRight); } Element::Action(s) => { write_element!(s, &MARGINS.action, Alignment::LeftToRight); } Element::Dialogue(dialogue) => { let premature_exit = write_dialogue( layout_info, dialogue, &mut residual_dialogue_idx, &mut residual_breakpoint_idx, max_lines_per_page, &mut line_idx, &mut surface, &MARGINS.dialogue, )?; if residual_dialogue_idx.is_some() || premature_exit { break; } } } ``` -------------------------------- ### pub fn export_head() -> String Source: https://docs.rs/rustwell/latest/rustwell/struct.HtmlExporter.html Exports only the head for the HTML output. ```APIDOC ## pub fn export_head() -> String ### Description Exports only the head for the `html` output. ### Method N/A (Rust function) ### Endpoint N/A (Rust function) ### Parameters None ### Request Example N/A ### Response #### Success Response - **String** - The head HTML content. #### Response Example N/A ### Error Handling N/A ``` -------------------------------- ### Element::new Source: https://docs.rs/rustwell/latest/rustwell/rich_string/struct.Element.html Creates a new Element from a String with no style attributes. Does not parse the string. ```APIDOC ## Element::new ### Description Creates a new element based on a String with no attributes. Does not parse the string. ### Method `pub fn new(text: String) -> Self` ### Parameters - **text** (String) - Required - The string content for the element. ### Returns - **Self** - A new Element instance with the given text and no style attributes. ``` -------------------------------- ### PDF export module imports and constants Source: https://docs.rs/rustwell/latest/src/rustwell/export/pdf.rs.html Imports required crates and defines constants for font size, font width, bundled fonts, paper sizes, and margins. ```rust use std::io::Write; use krilla::{ Document, color::rgb, destination::XyzDestination, geom::{PathBuilder, Point, Rect}, num::NormalizedF32, outline::{Outline, OutlineNode}, page::PageSettings, paint::Fill, surface::Surface, text::Font, }; use crate::{ Exporter, Screenplay, rich_string::RichString, screenplay::{Dialogue, DialogueElement, Element, Span, TitlePage}, }; const FONT_SIZE: usize = 12; // standard screenplay size const FONT_WIDTH: f32 = 7.2; // 12 * 0.6 (Courier Prime's aspect ratio) /// The font bundled together with Rustwell; Courier Prime. /// Includes the data of the font styles Regular, Bold, Italic /// and BoldItalic, in stated order. 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"), ]; ``` -------------------------------- ### pub fn export_css() -> &'static str Source: https://docs.rs/rustwell/latest/rustwell/struct.HtmlExporter.html Exports only the CSS for the HTML output. ```APIDOC ## pub fn export_css() -> &'static str ### Description Exports only the `css` for the `html` output. ### Method N/A (Rust function) ### Endpoint N/A (Rust function) ### Parameters None ### Request Example N/A ### Response #### Success Response - **&'static str** - The CSS content. #### Response Example N/A ### Error Handling N/A ``` -------------------------------- ### Rendering loop for screenplay elements in PDF export Source: https://docs.rs/rustwell/latest/src/rustwell/export/pdf.rs.html Handles each Element variant in the main rendering loop. For dual dialogue, it writes both columns with residual state and breaks if either has residual content. For synopsis, it optionally sets a gray fill. PageBreak advances the iterator and breaks. ```rust Element::DualDialogue(dialogue0, dialogue1) => { let mut initial_line_index = line_idx; let mut premature_exit = false; // Ensures dual dialogue is only written it either both have not been // written, or if this specifically is to be continued on a new page. if (residual_dual_dialogue_idx.0.is_none() && residual_dual_dialogue_idx.1.is_none()) || residual_dual_dialogue_idx.0.is_some() { premature_exit = premature_exit || write_dialogue( layout_info, dialogue0, &mut residual_dual_dialogue_idx.0, &mut residual_dual_breakpoint_idx.0, max_lines_per_page, &mut line_idx, &mut surface, &MARGINS.dual_dialogue.left, )?; } if (residual_dual_dialogue_idx.1.is_none() && residual_dual_dialogue_idx.0.is_none()) || residual_dual_dialogue_idx.1.is_some() { premature_exit = premature_exit || write_dialogue( layout_info, dialogue1, &mut residual_dual_dialogue_idx.1, &mut residual_dual_breakpoint_idx.1, max_lines_per_page, &mut initial_line_index, &mut surface, &MARGINS.dual_dialogue.right, )?; } line_idx = line_idx.max(initial_line_index); if residual_dual_dialogue_idx.0.is_some() || residual_dual_dialogue_idx.1.is_some() || premature_exit { break; } } Element::Lyrics(s) => { write_element!(s, &MARGINS.lyrics, Alignment::RightToLeft); } Element::Transition(s) => { write_element!(s, &MARGINS.transition, Alignment::RightToLeft); } Element::CenteredText(s) => { write_element!(s, &MARGINS.centered, Alignment::Centered); } Element::Synopsis(s) => { if self.synopses { surface.set_fill(Some(Fill { paint: rgb::Color::new(143, 143, 143).into(), opacity: NormalizedF32::new(0.5).unwrap(), rule: Default::default(), })); write_element!(s, &MARGINS.synopsis, Alignment::LeftToRight); surface.set_fill(None); } } Element::PageBreak => { element_iter.next(); break; } // Newline separator between all elements line_idx += 1; if residual_breakpoint_idx.is_some() { continue; } element_iter.next(); } surface.finish(); page.finish(); page_idx += 1; } document.set_outline(outline); Ok(()) } ``` -------------------------------- ### Preprocessor::process and find_earliest_token_of_interest Source: https://docs.rs/rustwell/latest/src/rustwell/parser.rs.html The main preprocessing loop that scans the input for tokens of interest (boneyards, notes, and newlines) and processes them accordingly. It handles boneyard removal, note state transitions, and newline handling, then returns the final result as a vector of lines. ```rust 558 buffer[0].0 = pre_line.0; 559 for (ln, line) in buffer { 560 self.result.push((ln, line)); 561 } 562 } 563 } 564 } 565 566 /// The main function for the preprocessor. 567 fn process(mut self) -> Vec { 568 while !self.rest.is_empty() { 569 let in_note = self.note_state.is_some(); 570 571 match Preprocessor::find_earliest_token_of_interest(self.rest, in_note) { 572 None => { 573 let remaining = self.rest.replace('\t', " "); 574 self.append_and_advance(&remaining); 575 break; 576 } 577 Some((pos, token)) => { 578 let before = self.rest[..pos].replace('\t', " "); 579 self.append_and_advance(&before); 580 self.rest = &self.rest[pos + token.len()..]; 581 582 match token { 583 "/*" => { 584 let (boneyard, after) = match self.rest.find("*/") { 585 Some(end) => (&self.rest[..end], &self.rest[end + 2..]), 586 None => (self.rest, ""), 587 }; 588 let newline_count = boneyard.chars().filter(|&c| c == '\n').count(); 589 self.source_line += newline_count; 590 591 self.rest = after; 592 } 593 "[[" => { 594 self.note_state = Some(NoteState { 595 buffer: vec![], 596 pre_line: std::mem::take(&mut self.current_line), 597 last_char_is_newline: false, 598 }); 599 self.current_line = (self.source_line, "[[".to_string()); 600 } 601 "]]" => { 602 self.current_line = self 603 .note_state 604 .take() 605 .map_or((self.source_line, String::new()), |n| n.pre_line); 606 } 607 "\n" => { 608 let Some(s) = &mut self.note_state else { 609 unreachable!( 610 "We only look for this pattern when note state is some." 611 ); 612 }; 613 614 if s.last_char_is_newline 615 && (pos == 0 616 || pos == 1 && matches!(before.chars().next(), Some(' '))) 617 { 618 self.dump_note_buffer(); 619 } else { 620 s.last_char_is_newline = true; 621 } 622 self.append_and_advance("\n"); 623 } 624 _ => unreachable!("Already checks all possible tokens"), 625 } 626 } 627 } 628 } 629 self.dump_note_buffer(); 630 if !self.current_line.1.is_empty() { 631 self.result.push(self.current_line); 632 } 633 self.result 634 } 635 636 /// Searches for the next potential token of interest. 637 /// Always looks for boneyards and other depends on if `in_note` or not. 638 fn find_earliest_token_of_interest(s: &str, in_note: bool) -> Option<(usize, &str)> { 639 let next_boneyard = s.find("/*"); 640 let next_note = if in_note { None } else { s.find("[[") }; 641 let next_note_end = if in_note { s.find("]]") } else { None }; 642 let next_note_break = if in_note { s.find('\n') } else { None }; 643 644 let mut candidates: Vec<(usize, &str)> = Vec::new(); 645 646 if let Some(p) = next_boneyard { 647 candidates.push((p, "/*")); 648 } 649 if let Some(p) = next_note { 650 candidates.push((p, "[[")); 651 } 652 if let Some(p) = next_note_end { candidates.push((p, "]]")); } if let Some(p) = next_note_break { candidates.push((p, "\n")); } candidates.into_iter().min_by_key(|&(p, _)| p) } } ``` -------------------------------- ### generate_pdf method for PdfExporter Source: https://docs.rs/rustwell/latest/src/rustwell/export/pdf.rs.html Implements the core PDF generation logic, iterating over screenplay elements and writing them page by page. Handles titlepage, page numbers, and residual elements that do not fit on a page. ```rust impl PdfExporter { /// Generates a `pdf` document from a [`Screenplay`]. Runs in (more or less) a single pass. fn generate_pdf( &self, document: &mut Document, layout_info: &LayoutInfo, screenplay: &Screenplay, ) -> std::io::Result<()> { let mut element_iter = screenplay.elements.iter().peekable(); // The index for which page in the document is currently being written. let mut page_idx = 0; // The maximum number of writable lines which can fit on a page, considering the top and // bottom margins. let max_lines_per_page = (layout_info.size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; // If an element does not fit within a page this will be Some(index), where index is pointing // to the breakpoint in the breakpoint list which should be on the start of the next page. let mut residual_breakpoint_idx = None; let mut residual_dialogue_idx = None; let mut residual_dual_dialogue_idx = (None, None); let mut residual_dual_breakpoint_idx = (None, None); let mut outline = Outline::new(); if let Some(t) = &screenplay.titlepage { page_idx += 1; write_titlepage(t, layout_info, max_lines_per_page, document)?; } // Page loop, creates a new page and writes everything it can on it. while element_iter.peek().is_some() { let mut page = document.start_page_with( PageSettings::from_wh(layout_info.size.x as f32, layout_info.size.y as f32) .unwrap(), ); let mut surface = page.surface(); let mut line_idx = 0; // Writes the page number. if (screenplay.titlepage.is_none() && page_idx > 0) || (screenplay.titlepage.is_some() && page_idx > 1) { let residual_page_number = write_element_custom_top_margin( layout_info, &format!( "{}.", if screenplay.titlepage.is_some() { page_idx } else { page_idx + 1 } ) .into(), &MARGINS.page_number, &mut 0, &mut 0, 1, &mut surface, Alignment::RightToLeft, 36, )?; // Page number cannot be larger than what fits on a single line on the page. if residual_page_number.is_some() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "There cannot be more pages than the number which fits on a page.", )); } } // Element loop, iterates through the screenplay elements. while let Some(Span { ``` -------------------------------- ### test_preprocessor macro and boneyard/note removal test cases Source: https://docs.rs/rustwell/latest/src/rustwell/parser.rs.html Defines a macro_rules! test_preprocessor that generates #[test] functions asserting Preprocessor::new($src).process() equals a vec of (line, expected) pairs. The macro is then invoked with a series of test cases covering boneyard (/* */) removal, note ([[ ]]) removal, line preservation, and edge cases such as unclosed notes, notes containing boneyards, and boneyards inside notes. Each invocation is a separate test case; the macro definition must be present for the invocations to compile. ```rust macro_rules! test_preprocessor { ($name:ident, $src:expr, [$(($line:expr, $expected:expr)),*]) => { #[test] fn $name() { assert_eq!( Preprocessor::new($src).process(), vec![$(($line, $expected.to_string())),*] ); } }; } ``` ```rust test_preprocessor!(trivial, "Hello\nWorld", [(1, "Hello"), (2, "World")]); ``` ```rust test_preprocessor!( boneyard_mid_line, "Hello /* removed */ World", [(1, "Hello World")] ); ``` ```rust test_preprocessor!( boneyard_line_start, "Hello\n/* removed */\nWorld", [(1, "Hello"), (2, ""), (3, "World")] ); ``` ```rust test_preprocessor!( boneyard_multiline_line_start, "Hello\n/* multi\nline\nboneyard */\nWorld", [(1, "Hello"), (2, ""), (5, "World")] ); ``` ```rust test_preprocessor!( boneyard_multiline_mid_line, "Hello /* multi\nline */ World", [(1, "Hello World")] ); ``` ```rust test_preprocessor!( note_removed, "Hello [[a note]] World", [(1, "Hello World")] ); ``` ```rust test_preprocessor!( note_multiline_removed, "Hello [[a\nnote]] World", [(1, "Hello World")] ); ``` ```rust test_preprocessor!( note_break_double_newline, "Hello [[a note\n\nWorld", [(1, "Hello [[a note"), (2, ""), (3, "World")] ); ``` ```rust test_preprocessor!( note_break_single_space_newline, "Hello [[a note\n \nWorld", [(1, "Hello [[a note"), (2, " "), (3, "World")] ); ``` ```rust test_preprocessor!( not_note_break_single_letter_newline, "Hello [[a note,\na\nWorld", [(1, "Hello [[a note,"), (2, "a"), (3, "World")] ); ``` ```rust test_preprocessor!(note_unclosed, "Hello [[a note", [(1, "Hello [[a note")]); ``` ```rust test_preprocessor!( note_with_boneyard_inside, "Hello [[a /* b1 */ note /* b2 */ here]] World", [(1, "Hello World")] ); ``` ```rust test_preprocessor!( boneyard_inside_note_breaks_note, "Hello [[a note\n/* boneyard */\nWorld", [(1, "Hello [[a note"), (2, ""), (3, "World")] ); ``` ```rust test_preprocessor!( mixed_boneyard_and_note, "/* boneyard */\n[[a note]]\nWorld", [(1, ""), (2, ""), (3, "World")] ); ``` ```rust test_preprocessor!( source_lines_preserved_after_boneyard, "Line1\n/*\n\n\n*/\nLine2", [(1, "Line1"), (2, ""), (6, "Line2")] ); ``` ```rust test_preprocessor!( source_lines_preserved_after_note, "Line1\n[[a\nmultiline\nnote]]\nLine2", [(1, "Line1"), (2, ""), (5, "Line2")] ); ``` -------------------------------- ### write_line: Draw text lines with alignment and underline in Rust Source: https://docs.rs/rustwell/latest/src/rustwell/export/pdf.rs.html The first block shows the tail of a loop that calls write_line with layout parameters and increments breakpoint and line indices. The second block is the write_line function itself, which handles newline characters, alignment adjustments, and draws text with optional underline using krilla's Surface and PathBuilder. ```rust layout_info, surface, left_margin, (FONT_SIZE * *line_index + top_margin) as f32, content, start_index, breakpoints.get(*breakpoint_index), text_direction, margin, )?; *breakpoint_index += 1; *line_index += 1; } Ok(None) } ``` ```rust fn write_line( layout_info: &LayoutInfo, surface: &mut Surface, mut x: f32, y: f32, content: &RichString, mut start_index: usize, // the "global" index to the rich string breakpoint: Option<&BreakPoint>, text_direction: Alignment, margin: &Margin, ) -> std::io::Result<()> { match content.get_char(start_index) { // Newlines are handled as part of the layout engine, so it should never write ut a newline // character. Some(c) => { if c == '\n' { start_index += 1 } } None => { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Could not get character from source.", )); } } let (breakpoint_index, break_word) = match breakpoint { Some(b) => (b.index, b.break_type == BreakType::BreakWord), None => (content.char_count(), false), }; // If we use a nonstandard [`Alignment`] we must move the `x` value of which we start writing // the line on. match text_direction { Alignment::LeftToRight => (), Alignment::RightToLeft => { let line_length = breakpoint_index - start_index; let line_span = line_length as f32 * FONT_WIDTH; x += layout_info.size.x as f32 - (margin.left + margin.right) - line_span; } Alignment::Centered => { let line_length = breakpoint_index - start_index; let line_span = (line_length / 2) as f32 * FONT_WIDTH; x = (layout_info.size.x / 2) as f32 - line_span; } } // Keeps track of the position on the line which the writer is currently at. let mut glyph_index = 0; // Writes until reaching the breakpoint position. while start_index < breakpoint_index { let (string_element, relative_index) = match content.get_element_from_index(start_index) { Some(res) => res, None => { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Could not get rich string element.", )); } }; let element_length = string_element.text.chars().count(); // The local index in the rich string element at which to break the line. let relative_break_index = if breakpoint_index - start_index >= element_length - relative_index { element_length } else { breakpoint_index - (start_index - relative_index) }; let font = match (string_element.is_bold(), string_element.is_italic()) { (false, false) => &layout_info.fonts.regular, (true, false) => &layout_info.fonts.bold, (false, true) => &layout_info.fonts.italic, (true, true) => &layout_info.fonts.bold_italic, }; let mut char_indices = string_element.text.char_indices(); let start_byte_index = char_indices.nth(relative_index).unwrap().0; let end_byte_index = match char_indices.nth(relative_break_index - relative_index - 1) { Some((i, _)) => i, None => string_element.text.len(), }; surface.draw_text( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), font.clone(), FONT_SIZE as f32, &string_element.text[start_byte_index..end_byte_index], false, krilla::text::TextDirection::LeftToRight, ); let glyphs_written = relative_break_index - relative_index; // Draws a line under the characters (to create an underline effect). if string_element.is_underline() { let underline = { let mut pb = PathBuilder::new(); let r = Rect::from_xywh( x + (glyph_index as f32 * FONT_WIDTH), y + 0.5, glyphs_written as f32 * FONT_WIDTH, 0.75, ) .unwrap(); pb.push_rect(r); pb.close(); ``` -------------------------------- ### LayoutInfo struct Source: https://docs.rs/rustwell/latest/src/rustwell/export/pdf.rs.html Defines the LayoutInfo struct that holds a reference to the paper size. ```rust struct LayoutInfo<'a> { pub size: &'a PaperSize, } ``` -------------------------------- ### PdfExporter Source: https://docs.rs/rustwell/latest/rustwell/struct.PdfExporter.html Documentation for the PdfExporter struct and its associated trait implementations, including the Exporter trait methods and the ExporterExt extension methods. ```APIDOC ## PdfExporter ### Description A `Screenplay` exporter for `pdf`. The variables configure the exporter. ### Fields - **synopses** (bool) - Whether to include synopses in the output. - **paper_size** (PaperSize) - What size (type) of paper (e.g. A4 or US letter). ### Trait Implementations #### Default for PdfExporter - **fn default() -> PdfExporter** - Returns the "default value" for a type. #### Exporter for PdfExporter - **fn file_extension(&self) -> &'static str** - The `.pdf` extension. - **fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> Result<()>** - Exports a `pdf` file and writes it to the provided writer. The pdf creation can fail if certain elements do not fit within a single page. #### ExporterExt for T (where T: Exporter + ?Sized) - **fn export_to_stdout(&self, screenplay: &Screenplay) -> Result<()>** - Exports the screenplay to stdout. - **fn export_to_file(&self, screenplay: &Screenplay, path: impl AsRef) -> Result<()>** - Exports the screenplay to a file at the given path. - **fn export_with_extension(&self, screenplay: &Screenplay, dir: impl AsRef, base: &str) -> Result<()>** - Exports the screenplay to `dir/base.ext` where `ext` is determined by `Exporter::file_extension`. - **fn export_to_string(&self, screenplay: &Screenplay) -> Result** - Exports the screenplay to a `String` in memory. ``` -------------------------------- ### test_screenplay macro and heading parsing test cases Source: https://docs.rs/rustwell/latest/src/rustwell/parser.rs.html Defines a macro_rules! test_screenplay that generates #[test] functions calling test_parse with the given input and expected elements. The helper test_parse parses the input and asserts each parsed element's inner value equals the expected element. The invocations cover heading parsing without a number, rejecting a line without a dot as an action, parsing a heading with a number, and a forced heading; the final invocation is truncated in the source. ```rust macro_rules! test_screenplay { ($name:ident, $input:expr, [$($elem:expr),*]) => { #[test] fn $name() { test_parse($input, [$($elem),*]); } }; } ``` ```rust fn test_parse(input: &str, expected: impl IntoIterator) { let parsed = parse(input); for ( Span { start_line: _, end_line: _, inner: actual, }, expected, ) in parsed.elements.iter().zip(expected) { assert_eq!(actual, &expected); } } ``` ```rust test_screenplay!( parses_heading_without_number, "InT. OUTSIDE - DAY", [Element::Heading { slug: "InT. OUTSIDE - DAY".into(), number: None, }] ); ``` ```rust test_screenplay!( does_not_parse_heading_whitout_dot, "Intro music plays.", [Element::Action("Intro music plays.".into())] ); ``` ```rust test_screenplay!( parses_heading_with_number, "INT. OUTSIDE - DAY #S.1#", [Element::Heading { slug: "INT. OUTSIDE - DAY".into(), number: Some("S.1".to_string()), }] ); ``` ```rust test_screenplay!( parses_heading_forced, ".OUTSIDE - DAY", [Element::Heading { ```