use super::{Paragraph, Table};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Page {
pub number: u32,
pub width: f32,
pub height: f32,
pub elements: Vec<Block>,
pub rotation: u16,
}
impl Page {
pub fn new(number: u32, width: f32, height: f32) -> Self {
Self {
number,
width,
height,
elements: Vec::new(),
rotation: 0,
}
}
pub fn letter(number: u32) -> Self {
Self::new(number, 612.0, 792.0) }
pub fn a4(number: u32) -> Self {
Self::new(number, 595.0, 842.0) }
pub fn add_block(&mut self, block: Block) {
self.elements.push(block);
}
pub fn add_paragraph(&mut self, paragraph: Paragraph) {
self.elements.push(Block::Paragraph(paragraph));
}
pub fn add_table(&mut self, table: Table) {
self.elements.push(Block::Table(table));
}
pub fn plain_text(&self) -> String {
self.elements
.iter()
.filter_map(|block| match block {
Block::Paragraph(p) => Some(p.plain_text()),
Block::Table(t) => Some(t.plain_text()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
pub fn block_count(&self) -> usize {
self.elements.len()
}
pub fn dimensions(&self) -> (f32, f32) {
(self.width, self.height)
}
pub fn is_landscape(&self) -> bool {
self.width > self.height
}
}
impl Default for Page {
fn default() -> Self {
Self::letter(1)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Block {
Paragraph(Paragraph),
Table(Table),
Image {
resource_id: String,
alt_text: Option<String>,
width: Option<f32>,
height: Option<f32>,
x: Option<f32>,
y: Option<f32>,
},
HorizontalRule,
PageBreak,
SectionBreak,
Raw {
content: String,
},
}
impl Block {
pub fn append_plain_text(&self, out: &mut String) {
match self {
Block::Paragraph(p) => out.push_str(&p.plain_text()),
Block::Table(t) => out.push_str(&t.plain_text()),
Block::Raw { content } => out.push_str(content),
_ => {}
}
}
pub fn image(resource_id: impl Into<String>) -> Self {
Block::Image {
resource_id: resource_id.into(),
alt_text: None,
width: None,
height: None,
x: None,
y: None,
}
}
pub fn image_with_size(resource_id: impl Into<String>, width: f32, height: f32) -> Self {
Block::Image {
resource_id: resource_id.into(),
alt_text: None,
width: Some(width),
height: Some(height),
x: None,
y: None,
}
}
pub fn is_paragraph(&self) -> bool {
matches!(self, Block::Paragraph(_))
}
pub fn is_table(&self) -> bool {
matches!(self, Block::Table(_))
}
pub fn is_image(&self) -> bool {
matches!(self, Block::Image { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_page_new() {
let page = Page::new(1, 612.0, 792.0);
assert_eq!(page.number, 1);
assert_eq!(page.width, 612.0);
assert_eq!(page.height, 792.0);
assert!(page.is_empty());
}
#[test]
fn test_page_letter_a4() {
let letter = Page::letter(1);
assert!(!letter.is_landscape());
let a4 = Page::a4(1);
assert!(!a4.is_landscape());
}
#[test]
fn test_block_variants() {
let img = Block::image("img1");
assert!(img.is_image());
assert!(!img.is_paragraph());
}
}