use super::{Paragraph, Resource, Table};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub keywords: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_modified_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub application: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub word_count: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Block {
Paragraph(Paragraph),
Table(Table),
PageBreak,
SectionBreak,
Image {
resource_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
alt_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<u32>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Section {
pub index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
pub content: Vec<Block>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header: Option<Vec<Paragraph>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub footer: Option<Vec<Paragraph>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<Vec<Paragraph>>,
}
impl Section {
pub fn new(index: usize) -> Self {
Self {
index,
..Default::default()
}
}
pub fn with_name(index: usize, name: impl Into<String>) -> Self {
Self {
index,
name: Some(name.into()),
..Default::default()
}
}
pub fn add_block(&mut self, block: Block) {
self.content.push(block);
}
pub fn add_paragraph(&mut self, para: Paragraph) {
self.content.push(Block::Paragraph(para));
}
pub fn add_table(&mut self, table: Table) {
self.content.push(Block::Table(table));
}
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub fn len(&self) -> usize {
self.content.len()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Document {
pub metadata: Metadata,
#[serde(default)]
pub sections: Vec<Section>,
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
pub resources: HashMap<String, Resource>,
}
impl Document {
pub fn new() -> Self {
Self::default()
}
pub fn add_section(&mut self, section: Section) {
self.sections.push(section);
}
pub fn add_resource(&mut self, id: impl Into<String>, resource: Resource) {
self.resources.insert(id.into(), resource);
}
pub fn get_resource(&self, id: &str) -> Option<&Resource> {
self.resources.get(id)
}
pub fn total_blocks(&self) -> usize {
self.sections.iter().map(|s| s.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.sections.is_empty() || self.sections.iter().all(|s| s.is_empty())
}
pub fn plain_text(&self) -> String {
let mut text = String::new();
for section in &self.sections {
for block in §ion.content {
match block {
Block::Paragraph(para) => {
text.push_str(¶.plain_text());
text.push('\n');
}
Block::Table(table) => {
text.push_str(&table.plain_text());
text.push('\n');
}
_ => {}
}
}
text.push('\n');
}
text.trim_matches('\n').to_string()
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_json_compact(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{RevisionType, TextRun, TextStyle};
#[test]
fn test_document_creation() {
let mut doc = Document::new();
assert!(doc.is_empty());
let mut section = Section::new(0);
let para = Paragraph {
runs: vec![TextRun::plain("Hello, World!")],
..Default::default()
};
section.add_paragraph(para);
doc.add_section(section);
assert!(!doc.is_empty());
assert_eq!(doc.total_blocks(), 1);
}
#[test]
fn test_plain_text_extraction() {
let mut doc = Document::new();
let mut section = Section::new(0);
section.add_paragraph(Paragraph {
runs: vec![
TextRun::plain("Hello, "),
TextRun {
text: "World".to_string(),
style: TextStyle {
bold: true,
..Default::default()
},
hyperlink: None,
line_break: false,
page_break: false,
revision: RevisionType::None,
},
TextRun::plain("!"),
],
..Default::default()
});
doc.add_section(section);
assert_eq!(doc.plain_text(), "Hello, World!");
}
#[test]
fn test_plain_text_preserves_boundary_spaces() {
let mut doc = Document::new();
let mut section = Section::new(0);
section.add_paragraph(Paragraph::with_text(" padded text "));
doc.add_section(section);
assert_eq!(doc.plain_text(), " padded text ");
}
#[test]
fn test_metadata_serialization() {
let meta = Metadata {
title: Some("Test Document".to_string()),
author: Some("Test Author".to_string()),
..Default::default()
};
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("Test Document"));
assert!(json.contains("Test Author"));
assert!(!json.contains("subject"));
}
#[test]
fn test_section_with_name() {
let section = Section::with_name(0, "Sheet1");
assert_eq!(section.name, Some("Sheet1".to_string()));
assert_eq!(section.index, 0);
}
}