use super::font_manager::FontWeight;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placeholder {
PageNumber,
TotalPages,
Date,
Time,
Title,
Author,
}
impl Placeholder {
pub fn token(&self) -> &'static str {
match self {
Placeholder::PageNumber => "{page}",
Placeholder::TotalPages => "{pages}",
Placeholder::Date => "{date}",
Placeholder::Time => "{time}",
Placeholder::Title => "{title}",
Placeholder::Author => "{author}",
}
}
pub fn parse_all(text: &str) -> Vec<(usize, Placeholder)> {
let mut placeholders = Vec::new();
for ph in [
Placeholder::PageNumber,
Placeholder::TotalPages,
Placeholder::Date,
Placeholder::Time,
Placeholder::Title,
Placeholder::Author,
] {
let token = ph.token();
let mut start = 0;
while let Some(pos) = text[start..].find(token) {
placeholders.push((start + pos, ph));
start += pos + token.len();
}
}
placeholders.sort_by_key(|(pos, _)| *pos);
placeholders
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ArtifactAlignment {
Left,
#[default]
Center,
Right,
}
#[derive(Debug, Clone)]
pub struct ArtifactStyle {
pub font_name: String,
pub font_size: f32,
pub font_weight: FontWeight,
pub color: (f32, f32, f32),
pub separator_line: bool,
pub separator_width: f32,
}
impl Default for ArtifactStyle {
fn default() -> Self {
Self {
font_name: "Helvetica".to_string(),
font_size: 10.0,
font_weight: FontWeight::Normal,
color: (0.0, 0.0, 0.0), separator_line: false,
separator_width: 0.5,
}
}
}
impl ArtifactStyle {
pub fn new() -> Self {
Self::default()
}
pub fn font(mut self, name: impl Into<String>, size: f32) -> Self {
self.font_name = name.into();
self.font_size = size;
self
}
pub fn bold(mut self) -> Self {
self.font_weight = FontWeight::Bold;
self
}
pub fn color(mut self, r: f32, g: f32, b: f32) -> Self {
self.color = (r, g, b);
self
}
pub fn with_separator(mut self, width: f32) -> Self {
self.separator_line = true;
self.separator_width = width;
self
}
}
#[derive(Debug, Clone)]
pub struct ArtifactElement {
pub text: String,
pub alignment: ArtifactAlignment,
pub style: Option<ArtifactStyle>,
}
impl ArtifactElement {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
alignment: ArtifactAlignment::Center,
style: None,
}
}
pub fn left(text: impl Into<String>) -> Self {
Self {
text: text.into(),
alignment: ArtifactAlignment::Left,
style: None,
}
}
pub fn center(text: impl Into<String>) -> Self {
Self {
text: text.into(),
alignment: ArtifactAlignment::Center,
style: None,
}
}
pub fn right(text: impl Into<String>) -> Self {
Self {
text: text.into(),
alignment: ArtifactAlignment::Right,
style: None,
}
}
pub fn with_style(mut self, style: ArtifactStyle) -> Self {
self.style = Some(style);
self
}
pub fn resolve(&self, context: &PlaceholderContext) -> String {
let mut result = self.text.clone();
result = result.replace(Placeholder::PageNumber.token(), &context.page_number.to_string());
result = result.replace(Placeholder::TotalPages.token(), &context.total_pages.to_string());
result = result.replace(Placeholder::Date.token(), &context.date);
result = result.replace(Placeholder::Time.token(), &context.time);
result = result.replace(Placeholder::Title.token(), &context.title);
result = result.replace(Placeholder::Author.token(), &context.author);
result
}
}
#[derive(Debug, Clone, Default)]
pub struct Artifact {
pub left: Option<ArtifactElement>,
pub center: Option<ArtifactElement>,
pub right: Option<ArtifactElement>,
pub style: ArtifactStyle,
pub offset: f32,
}
pub type Header = Artifact;
pub type Footer = Artifact;
impl Artifact {
pub fn new() -> Self {
Self {
offset: 36.0, ..Default::default()
}
}
pub fn left(text: impl Into<String>) -> Self {
let mut hf = Self::new();
hf.left = Some(ArtifactElement::left(text));
hf
}
pub fn center(text: impl Into<String>) -> Self {
let mut hf = Self::new();
hf.center = Some(ArtifactElement::center(text));
hf
}
pub fn right(text: impl Into<String>) -> Self {
let mut hf = Self::new();
hf.right = Some(ArtifactElement::right(text));
hf
}
pub fn with_left(mut self, text: impl Into<String>) -> Self {
self.left = Some(ArtifactElement::left(text));
self
}
pub fn with_center(mut self, text: impl Into<String>) -> Self {
self.center = Some(ArtifactElement::center(text));
self
}
pub fn with_right(mut self, text: impl Into<String>) -> Self {
self.right = Some(ArtifactElement::right(text));
self
}
pub fn with_style(mut self, style: ArtifactStyle) -> Self {
self.style = style;
self
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.offset = offset;
self
}
pub fn is_empty(&self) -> bool {
self.left.is_none() && self.center.is_none() && self.right.is_none()
}
pub fn elements(&self) -> Vec<&ArtifactElement> {
let mut elements = Vec::new();
if let Some(ref e) = self.left {
elements.push(e);
}
if let Some(ref e) = self.center {
elements.push(e);
}
if let Some(ref e) = self.right {
elements.push(e);
}
elements
}
}
#[derive(Debug, Clone)]
pub struct PlaceholderContext {
pub page_number: usize,
pub total_pages: usize,
pub date: String,
pub time: String,
pub title: String,
pub author: String,
}
impl PlaceholderContext {
pub fn new(page_number: usize, total_pages: usize) -> Self {
let now = chrono::Local::now();
Self {
page_number,
total_pages,
date: now.format("%Y-%m-%d").to_string(),
time: now.format("%H:%M").to_string(),
title: String::new(),
author: String::new(),
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.author = author.into();
self
}
}
impl Default for PlaceholderContext {
fn default() -> Self {
Self::new(1, 1)
}
}
#[derive(Debug, Clone, Default)]
pub struct PageTemplate {
pub header: Option<Artifact>,
pub footer: Option<Artifact>,
pub skip_first_page: bool,
pub first_page_header: Option<Artifact>,
pub first_page_footer: Option<Artifact>,
pub margin_left: f32,
pub margin_right: f32,
}
impl PageTemplate {
pub fn new() -> Self {
Self {
margin_left: 72.0, margin_right: 72.0, ..Default::default()
}
}
pub fn header(mut self, header: Artifact) -> Self {
self.header = Some(header);
self
}
pub fn footer(mut self, footer: Artifact) -> Self {
self.footer = Some(footer);
self
}
pub fn skip_first_page(mut self) -> Self {
self.skip_first_page = true;
self
}
pub fn first_page_header(mut self, header: Artifact) -> Self {
self.first_page_header = Some(header);
self
}
pub fn first_page_footer(mut self, footer: Artifact) -> Self {
self.first_page_footer = Some(footer);
self
}
pub fn margins(mut self, left: f32, right: f32) -> Self {
self.margin_left = left;
self.margin_right = right;
self
}
pub fn get_header(&self, page_number: usize) -> Option<&Artifact> {
if page_number == 1 {
if self.skip_first_page && self.first_page_header.is_none() {
return None;
}
self.first_page_header.as_ref().or(self.header.as_ref())
} else {
self.header.as_ref()
}
}
pub fn get_footer(&self, page_number: usize) -> Option<&Artifact> {
if page_number == 1 {
if self.skip_first_page && self.first_page_footer.is_none() {
return None;
}
self.first_page_footer.as_ref().or(self.footer.as_ref())
} else {
self.footer.as_ref()
}
}
pub fn is_empty(&self) -> bool {
self.header.is_none()
&& self.footer.is_none()
&& self.first_page_header.is_none()
&& self.first_page_footer.is_none()
}
}
pub struct PageNumberFormat;
impl PageNumberFormat {
pub fn page_x() -> String {
"Page {page}".to_string()
}
pub fn page_x_of_y() -> String {
"Page {page} of {pages}".to_string()
}
pub fn x_slash_y() -> String {
"{page} / {pages}".to_string()
}
pub fn number_only() -> String {
"{page}".to_string()
}
pub fn dashed() -> String {
"- {page} -".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_placeholder_tokens() {
assert_eq!(Placeholder::PageNumber.token(), "{page}");
assert_eq!(Placeholder::TotalPages.token(), "{pages}");
assert_eq!(Placeholder::Date.token(), "{date}");
}
#[test]
fn test_placeholder_parse() {
let text = "Page {page} of {pages}";
let placeholders = Placeholder::parse_all(text);
assert_eq!(placeholders.len(), 2);
assert_eq!(placeholders[0].1, Placeholder::PageNumber);
assert_eq!(placeholders[1].1, Placeholder::TotalPages);
}
#[test]
fn test_hf_element_resolve() {
let element = ArtifactElement::center("Page {page} of {pages}");
let context = PlaceholderContext::new(5, 10);
let resolved = element.resolve(&context);
assert_eq!(resolved, "Page 5 of 10");
}
#[test]
fn test_artifact_creation() {
let hf = Artifact::new()
.with_left("Document Title")
.with_right("{page}");
assert!(hf.left.is_some());
assert!(hf.center.is_none());
assert!(hf.right.is_some());
}
#[test]
fn test_page_template() {
let template = PageTemplate::new()
.header(Artifact::center("My Document"))
.footer(Artifact::right("{page} of {pages}"));
assert!(template.header.is_some());
assert!(template.footer.is_some());
assert!(!template.is_empty());
}
#[test]
fn test_skip_first_page() {
let template = PageTemplate::new()
.header(Artifact::center("Header"))
.skip_first_page();
assert!(template.get_header(1).is_none());
assert!(template.get_header(2).is_some());
}
#[test]
fn test_first_page_template() {
let template = PageTemplate::new()
.header(Artifact::center("Regular Header"))
.first_page_header(Artifact::center("Title Page"));
let first = template.get_header(1).unwrap();
let second = template.get_header(2).unwrap();
assert_eq!(first.center.as_ref().unwrap().text, "Title Page");
assert_eq!(second.center.as_ref().unwrap().text, "Regular Header");
}
#[test]
fn test_page_number_formats() {
assert_eq!(PageNumberFormat::page_x(), "Page {page}");
assert_eq!(PageNumberFormat::page_x_of_y(), "Page {page} of {pages}");
assert_eq!(PageNumberFormat::x_slash_y(), "{page} / {pages}");
}
#[test]
fn test_hf_style() {
let style = ArtifactStyle::new()
.font("Times-Roman", 12.0)
.bold()
.color(0.5, 0.5, 0.5)
.with_separator(1.0);
assert_eq!(style.font_name, "Times-Roman");
assert_eq!(style.font_size, 12.0);
assert!(matches!(style.font_weight, FontWeight::Bold));
assert!(style.separator_line);
}
#[test]
fn test_placeholder_context_with_metadata() {
let context = PlaceholderContext::new(1, 10)
.with_title("My Document")
.with_author("John Doe");
let element = ArtifactElement::center("{title} by {author}");
let resolved = element.resolve(&context);
assert_eq!(resolved, "My Document by John Doe");
}
}