use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Paragraph {
pub content: Vec<InlineContent>,
pub style: ParagraphStyle,
}
impl Paragraph {
pub fn new() -> Self {
Self {
content: Vec::new(),
style: ParagraphStyle::default(),
}
}
pub fn with_text(text: impl Into<String>) -> Self {
let mut p = Self::new();
p.add_text(text);
p
}
pub fn heading(text: impl Into<String>, level: u8) -> Self {
let mut p = Self::with_text(text);
p.style.heading_level = Some(level.clamp(1, 6));
p
}
pub fn add_text(&mut self, text: impl Into<String>) {
self.content.push(InlineContent::Text(TextRun {
text: text.into(),
style: TextStyle::default(),
}));
}
pub fn add_run(&mut self, run: TextRun) {
self.content.push(InlineContent::Text(run));
}
pub fn add_line_break(&mut self) {
self.content.push(InlineContent::LineBreak);
}
pub fn plain_text(&self) -> String {
self.content
.iter()
.map(|c| match c {
InlineContent::Text(run) => run.text.clone(),
InlineContent::LineBreak => "\n".to_string(),
InlineContent::Link { text, .. } => text.clone(),
InlineContent::Image { alt_text, .. } => alt_text.clone().unwrap_or_default(),
})
.collect()
}
pub fn is_empty(&self) -> bool {
self.content.is_empty() || self.plain_text().trim().is_empty()
}
pub fn is_heading(&self) -> bool {
self.style.heading_level.is_some()
}
pub fn heading_level(&self) -> Option<u8> {
self.style.heading_level
}
pub fn is_list_item(&self) -> bool {
self.style.list_info.is_some()
}
}
impl Default for Paragraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InlineContent {
Text(TextRun),
LineBreak,
Link {
text: String,
url: String,
title: Option<String>,
},
Image {
resource_id: String,
alt_text: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextRun {
pub text: String,
pub style: TextStyle,
}
impl TextRun {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
style: TextStyle::default(),
}
}
pub fn bold(text: impl Into<String>) -> Self {
Self {
text: text.into(),
style: TextStyle {
bold: true,
..Default::default()
},
}
}
pub fn italic(text: impl Into<String>) -> Self {
Self {
text: text.into(),
style: TextStyle {
italic: true,
..Default::default()
},
}
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TextStyle {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub superscript: bool,
pub subscript: bool,
pub font_name: Option<String>,
pub font_size: Option<f32>,
pub color: Option<String>,
pub background_color: Option<String>,
}
impl TextStyle {
pub fn has_styling(&self) -> bool {
self.bold
|| self.italic
|| self.underline
|| self.strikethrough
|| self.superscript
|| self.subscript
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ParagraphStyle {
pub heading_level: Option<u8>,
pub alignment: Alignment,
pub indent_level: u8,
pub list_info: Option<ListInfo>,
pub line_spacing: Option<f32>,
pub space_before: Option<f32>,
pub space_after: Option<f32>,
pub first_line_indent: Option<f32>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Alignment {
#[default]
Left,
Center,
Right,
Justify,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListInfo {
pub style: ListStyle,
pub level: u8,
pub item_number: Option<u32>,
}
impl ListInfo {
pub fn bullet(level: u8) -> Self {
Self {
style: ListStyle::Unordered { marker: '•' },
level,
item_number: None,
}
}
pub fn numbered(level: u8, number: u32) -> Self {
Self {
style: ListStyle::Ordered {
start: 1,
number_style: NumberStyle::Decimal,
},
level,
item_number: Some(number),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ListStyle {
Ordered {
start: u32,
number_style: NumberStyle,
},
Unordered {
marker: char,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NumberStyle {
#[default]
Decimal,
LowerAlpha,
UpperAlpha,
LowerRoman,
UpperRoman,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_paragraph_plain_text() {
let mut p = Paragraph::new();
p.add_text("Hello ");
p.add_run(TextRun::bold("world"));
p.add_text("!");
assert_eq!(p.plain_text(), "Hello world!");
}
#[test]
fn test_heading() {
let h1 = Paragraph::heading("Title", 1);
assert!(h1.is_heading());
assert_eq!(h1.heading_level(), Some(1));
}
#[test]
fn test_text_style() {
let style = TextStyle::default();
assert!(!style.has_styling());
let bold_style = TextStyle {
bold: true,
..Default::default()
};
assert!(bold_style.has_styling());
}
#[test]
fn test_list_info() {
let bullet = ListInfo::bullet(0);
assert_eq!(bullet.level, 0);
let numbered = ListInfo::numbered(1, 5);
assert_eq!(numbered.item_number, Some(5));
}
}