docxcraft 0.1.0

Generate editable DOCX documents with rich text, tables, charts and SVG drawings
Documentation
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 ALGFORCE INC.

use crate::get_font_for_locale;

/// 表示一个文档段落
#[derive(Debug, Clone, Default)]
pub struct ParagraphStyle {
    pub font: Option<String>,
    pub size: Option<u32>,
    pub color: Option<String>,
    pub alignment: Option<String>,
    pub indent: Option<u32>, // 单位: twentieths of a point (docx标准)
    pub bold: Option<bool>,  // 是否加粗
    pub locale: String,
}
impl ParagraphStyle {
    pub fn new() -> Self {
        ParagraphStyle {
            font: None,
            size: Some(10),
            color: None,
            alignment: None,
            indent: None,
            bold: None,
            locale: "en".to_string(),
        }
    }

    /// 根据 locale 设置默认字体
    pub fn with_locale(mut self, locale: String) -> Self {
        self.locale = locale.clone();
        if self.font.is_none() {
            self.font = Some(get_font_for_locale(&locale));
        }
        self
    }

    /// 设置默认字体
    pub fn with_font(mut self, font: String) -> Self {
        self.font = Some(font);
        self
    }
}

#[derive(Debug, Clone)]
pub struct Paragraph {
    pub text: String,
    pub style: Option<ParagraphStyle>,
}

impl Paragraph {
    pub fn new(text: String) -> Self {
        Self {
            text,
            style: Some(ParagraphStyle::new()),
        }
    }

    /// 根据 locale 设置默认字体
    pub fn with_locale(mut self, locale: String) -> Self {
        self.style = Some(self.style.unwrap_or_default().with_locale(locale));
        self
    }

    pub fn with_font(mut self, font: String) -> Self {
        self.style.get_or_insert_with(ParagraphStyle::default).font = Some(font);
        self
    }

    pub fn with_size(mut self, size: u32) -> Self {
        self.style.get_or_insert_with(ParagraphStyle::default).size = Some(size);
        self
    }

    pub fn with_color(mut self, color: String) -> Self {
        self.style.get_or_insert_with(ParagraphStyle::default).color = Some(color);
        self
    }

    pub fn with_alignment(mut self, alignment: String) -> Self {
        self.style
            .get_or_insert_with(ParagraphStyle::default)
            .alignment = Some(alignment);
        self
    }

    pub fn with_indent(mut self, indent: u32) -> Self {
        self.style
            .get_or_insert_with(ParagraphStyle::default)
            .indent = Some(indent);
        self
    }

    pub fn with_bold(mut self, bold: bool) -> Self {
        self.style.get_or_insert_with(ParagraphStyle::default).bold = Some(bold);
        self
    }

    /// 生成段落的XML表示
    pub fn generate_paragraph_xml(&self, escape_fn: fn(&str) -> String) -> String {
        let mut rpr_content = String::new();
        let mut ppr_content = String::new();

        if let Some(style) = &self.style {
            // 获取字体名称,如果没有设置则使用 locale 对应的默认字体
            let font_name = style
                .font
                .as_ref()
                .cloned()
                .unwrap_or_else(|| get_font_for_locale(&style.locale));

            rpr_content.push_str(&format!(
                r#"<w:rFonts w:ascii="{}" w:hAnsi="{}" w:eastAsia="{}"/>"#,
                font_name, font_name, font_name
            ));

            if let Some(font_size) = style.size {
                rpr_content.push_str(&format!(r#"<w:sz w:val="{}"/>"#, font_size * 2));
            }

            if let Some(font_color) = &style.color {
                rpr_content.push_str(&format!(r#"<w:color w:val="{}"/>"#, font_color));
            }

            if let Some(bold) = style.bold {
                if bold {
                    rpr_content.push_str(r#"<w:b/>"#);
                }
            }

            if let Some(alignment) = &style.alignment {
                ppr_content.push_str(&format!(r#"<w:jc w:val="{}"/>"#, alignment));
            }
            if let Some(indent) = style.indent {
                ppr_content.push_str(&format!(r#"<w:ind w:firstLine="{}"/>"#, indent));
            }
        }

        let r_pr_xml = if !rpr_content.is_empty() {
            format!("<w:rPr>{}</w:rPr>", rpr_content)
        } else {
            String::new()
        };

        let p_pr_xml = if !ppr_content.is_empty() {
            format!("<w:pPr>{}</w:pPr>", ppr_content)
        } else {
            String::new()
        };

        format!(
            r#"
    <w:p>
      {}
      <w:r>
        {}{}
      </w:r>
    </w:p>"#,
            p_pr_xml,
            r_pr_xml,
            format_args!(r#"<w:t>{}</w:t>"#, escape_fn(&self.text))
        )
    }
}