use crate::block::{Block, BlockType, ButtonStyle, CalloutType, EmbedType, ListType};
use crate::document::Document;
use crate::error::Result;
use std::collections::HashMap;
pub struct BlockBuilder {
block_type: BlockType,
content: String,
metadata: HashMap<String, String>,
}
impl BlockBuilder {
pub fn text(content: impl Into<String>) -> Self {
Self {
block_type: BlockType::Text,
content: content.into(),
metadata: HashMap::new(),
}
}
pub fn header(level: u8, content: impl Into<String>) -> Result<Self> {
if !(1..=6).contains(&level) {
return Err(crate::error::BlocksError::InvalidHeaderLevel { level });
}
Ok(Self {
block_type: BlockType::Header { level },
content: content.into(),
metadata: HashMap::new(),
})
}
pub fn code(content: impl Into<String>, language: Option<String>) -> Self {
Self {
block_type: BlockType::Code { language },
content: content.into(),
metadata: HashMap::new(),
}
}
pub fn quote(content: impl Into<String>) -> Self {
Self {
block_type: BlockType::Quote,
content: content.into(),
metadata: HashMap::new(),
}
}
pub fn list(content: impl Into<String>, list_type: ListType) -> Self {
Self {
block_type: BlockType::List { list_type },
content: content.into(),
metadata: HashMap::new(),
}
}
pub fn unordered_list(content: impl Into<String>) -> Self {
Self::list(content, ListType::Unordered)
}
pub fn ordered_list(content: impl Into<String>) -> Self {
Self::list(content, ListType::Ordered)
}
pub fn link(
text: impl Into<String>,
url: impl Into<String>,
title: Option<String>,
) -> Result<Self> {
let url = url.into();
crate::sanitizer::ContentSanitizer::new().validate_url(&url)?;
Ok(Self {
block_type: BlockType::Link { url, title },
content: text.into(),
metadata: HashMap::new(),
})
}
pub fn image(
url: impl Into<String>,
alt: impl Into<String>,
caption: Option<String>,
) -> Result<Self> {
let url = url.into();
crate::sanitizer::ContentSanitizer::new().validate_url(&url)?;
Ok(Self {
block_type: BlockType::Image {
url,
alt: alt.into(),
caption,
},
content: String::new(),
metadata: HashMap::new(),
})
}
pub fn button(
text: impl Into<String>,
url: impl Into<String>,
style: ButtonStyle,
) -> Result<Self> {
let url = url.into();
crate::sanitizer::ContentSanitizer::new().validate_url(&url)?;
Ok(Self {
block_type: BlockType::Button {
text: text.into(),
url,
style,
},
content: String::new(),
metadata: HashMap::new(),
})
}
pub fn callout(
callout_type: CalloutType,
title: Option<String>,
content: impl Into<String>,
) -> Self {
Self {
block_type: BlockType::Callout {
callout_type,
title,
},
content: content.into(),
metadata: HashMap::new(),
}
}
pub fn divider() -> Self {
Self {
block_type: BlockType::Divider,
content: String::new(),
metadata: HashMap::new(),
}
}
pub fn table(headers: Vec<String>, rows: Vec<Vec<String>>, has_header: bool) -> Self {
Self {
block_type: BlockType::Table {
headers,
rows,
has_header,
},
content: String::new(),
metadata: HashMap::new(),
}
}
pub fn embed(
embed_type: EmbedType,
url: impl Into<String>,
width: Option<u32>,
height: Option<u32>,
) -> Result<Self> {
let url = url.into();
crate::sanitizer::ContentSanitizer::new().validate_url(&url)?;
Ok(Self {
block_type: BlockType::Embed {
embed_type,
url,
width,
height,
},
content: String::new(),
metadata: HashMap::new(),
})
}
pub fn details(summary: impl Into<String>, content: impl Into<String>, is_open: bool) -> Self {
Self {
block_type: BlockType::Details {
summary: summary.into(),
is_open,
},
content: content.into(),
metadata: HashMap::new(),
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata.extend(metadata);
self
}
pub fn build(self) -> Block {
Block::new_with_metadata(self.block_type, self.content, self.metadata)
}
}
pub struct DocumentBuilder {
title: String,
blocks: Vec<Block>,
metadata: HashMap<String, String>,
}
impl DocumentBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
blocks: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn empty() -> Self {
Self {
title: String::new(),
blocks: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn with_block(mut self, block: Block) -> Self {
self.blocks.push(block);
self
}
pub fn with_blocks(mut self, blocks: Vec<Block>) -> Self {
self.blocks.extend(blocks);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata.extend(metadata);
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn build(self) -> Document {
let mut doc = if self.title.is_empty() {
Document::new()
} else {
Document::with_title(self.title)
};
for block in self.blocks {
doc.add_block(block);
}
doc.metadata.extend(self.metadata);
doc
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_block_builder() {
let block = BlockBuilder::text("Hello World").build();
assert_eq!(block.content, "Hello World");
assert_eq!(block.block_type, BlockType::Text);
}
#[test]
fn test_header_block_builder() {
let block = BlockBuilder::header(1, "Title").unwrap().build();
assert_eq!(block.content, "Title");
match block.block_type {
BlockType::Header { level } => assert_eq!(level, 1),
_ => panic!("Expected Header block"),
}
}
#[test]
fn test_invalid_header_level() {
assert!(BlockBuilder::header(7, "Title").is_err());
assert!(BlockBuilder::header(0, "Title").is_err());
}
#[test]
fn test_code_block_builder() {
let block = BlockBuilder::code("print('hello')", Some("python".to_string())).build();
assert_eq!(block.content, "print('hello')");
match block.block_type {
BlockType::Code { language } => assert_eq!(language, Some("python".to_string())),
_ => panic!("Expected Code block"),
}
}
#[test]
fn test_list_block_builder() {
let block = BlockBuilder::unordered_list("item1\nitem2\nitem3").build();
assert!(block.content.contains("item1"));
match block.block_type {
BlockType::List { list_type } => assert_eq!(list_type, ListType::Unordered),
_ => panic!("Expected List block"),
}
}
#[test]
fn test_block_with_metadata() {
let block = BlockBuilder::text("Hello")
.with_metadata("author", "John")
.with_metadata("version", "1.0")
.build();
assert_eq!(block.metadata.get("author"), Some(&"John".to_string()));
assert_eq!(block.metadata.get("version"), Some(&"1.0".to_string()));
}
#[test]
fn test_document_builder() {
let doc = DocumentBuilder::new("My Doc")
.with_block(BlockBuilder::text("Hello").build())
.with_block(BlockBuilder::text("World").build())
.build();
assert_eq!(doc.title, "My Doc");
assert_eq!(doc.blocks.len(), 2);
}
#[test]
fn test_document_builder_with_metadata() {
let doc = DocumentBuilder::new("My Doc")
.with_metadata("author", "Alice")
.with_metadata("version", "2.0")
.build();
assert_eq!(doc.metadata.get("author"), Some(&"Alice".to_string()));
assert_eq!(doc.metadata.get("version"), Some(&"2.0".to_string()));
}
#[test]
fn test_quote_block_builder() {
let block = BlockBuilder::quote("A great quote").build();
assert_eq!(block.content, "A great quote");
assert_eq!(block.block_type, BlockType::Quote);
}
#[test]
fn test_divider_block_builder() {
let block = BlockBuilder::divider().build();
assert_eq!(block.block_type, BlockType::Divider);
}
#[test]
fn test_link_block_builder() {
let block = BlockBuilder::link("Click here", "https://example.com", None)
.unwrap()
.build();
assert_eq!(block.content, "Click here");
match block.block_type {
BlockType::Link { url, title } => {
assert_eq!(url, "https://example.com");
assert_eq!(title, None);
}
_ => panic!("Expected Link block"),
}
}
#[test]
fn test_button_block_builder() {
let block = BlockBuilder::button("Click", "https://example.com", ButtonStyle::Primary)
.unwrap()
.build();
assert_eq!(block.content, "");
match block.block_type {
BlockType::Button { text, url, style } => {
assert_eq!(text, "Click");
assert_eq!(url, "https://example.com");
assert_eq!(style, ButtonStyle::Primary);
}
_ => panic!("Expected Button block"),
}
}
}