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 anyhow::Result;
use serde_json::{from_str, Value};

use crate::get_font_for_locale;

/// Represents a cell in a table
#[derive(Debug)]
pub struct TableCell {
    content: String,
    is_header: bool,
}

impl TableCell {
    pub fn new(content: &str) -> Self {
        Self {
            content: content.to_string(),
            is_header: false,
        }
    }

    pub fn header(content: &str) -> Self {
        Self {
            content: content.to_string(),
            is_header: true,
        }
    }
}

/// Represents a row in a table
#[derive(Debug)]
pub struct TableRow {
    cells: Vec<TableCell>,
}

impl TableRow {
    pub fn new() -> Self {
        Self { cells: Vec::new() }
    }

    pub fn add_cell(&mut self, cell: TableCell) {
        self.cells.push(cell);
    }
}

/// Represents a table in a document
#[derive(Debug)]
pub struct Table {
    rows: Vec<TableRow>,
    width: u32,
    default_font: String,
    font_size: u32,
    font_color: String,
    font_bold: bool,
    has_custom_typography: bool,
    locale: String,
}

impl Table {
    pub fn new() -> Self {
        Self {
            rows: Vec::new(),
            width: 0,
            default_font: "Arial".to_owned(),
            font_size: 10,
            font_color: "000000".to_string(),
            font_bold: false,
            has_custom_typography: false,
            locale: "en".to_string(),
        }
    }

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

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

    pub fn with_typography(mut self, font: String, size: u32, color: String, bold: bool) -> Self {
        self.default_font = font;
        self.font_size = size;
        self.font_color = color.trim_start_matches('#').to_owned();
        self.font_bold = bold;
        self.has_custom_typography = true;
        self
    }

    pub fn add_row(&mut self, row: TableRow) {
        if self.width == 0 && !row.cells.is_empty() {
            self.width = row.cells.len() as u32;
        }
        self.rows.push(row);
    }

    /// Generate OpenXML for a table
    pub fn generate_table_xml(&self) -> String {
        let mut xml = String::new();

        // Start table
        xml.push_str("<w:tbl>");

        // Table properties
        xml.push_str("<w:tblPr>");
        xml.push_str("<w:tblStyle w:val=\"TableGrid\"/>");
        xml.push_str("<w:tblW w:w=\"5000\" w:type=\"pct\"/>");
        xml.push_str("<w:tblLook w:val=\"04A0\" w:firstRow=\"1\" w:lastRow=\"0\" w:firstColumn=\"1\" w:lastColumn=\"0\" w:noHBand=\"0\" w:noVBand=\"1\"/>");
        xml.push_str("</w:tblPr>");

        // Table grid (column definitions)
        xml.push_str("<w:tblGrid>");
        for _ in 0..self.width {
            xml.push_str("<w:gridCol w:w=\"2500\"/>");
        }
        xml.push_str("</w:tblGrid>");

        // Table rows
        for row in &self.rows {
            xml.push_str("<w:tr>");

            for cell in &row.cells {
                xml.push_str("<w:tc>");

                // Cell properties
                xml.push_str("<w:tcPr>");
                if cell.is_header {
                    xml.push_str("<w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"E7E6E6\"/>");
                }
                xml.push_str("</w:tcPr>");

                // Cell content
                xml.push_str("<w:p>");
                xml.push_str("<w:pPr>");
                if cell.is_header {
                    xml.push_str("<w:b/>");
                }
                xml.push_str("</w:pPr>");
                xml.push_str("<w:r>");
                xml.push_str("<w:rPr>");
                // 设置字体
                xml.push_str(&format!(
                    "<w:rFonts w:ascii=\"{}\" w:hAnsi=\"{}\" w:eastAsia=\"{}\"/>",
                    self.default_font, self.default_font, self.default_font
                ));
                let half_points = self.font_size.saturating_mul(2);
                xml.push_str(&format!("<w:sz w:val=\"{}\"/>", half_points));
                xml.push_str(&format!("<w:szCs w:val=\"{}\"/>", half_points));
                xml.push_str(&format!("<w:color w:val=\"{}\"/>", self.font_color));
                if cell.is_header || self.font_bold {
                    xml.push_str("<w:b/>");
                }
                xml.push_str("</w:rPr>");
                xml.push_str("<w:t>");
                let escaped_content = self.escape_xml(&cell.content);
                xml.push_str(&escaped_content);
                xml.push_str("</w:t>");
                xml.push_str("</w:r>");
                xml.push_str("</w:p>");

                xml.push_str("</w:tc>");
            }

            xml.push_str("</w:tr>");
        }

        // End table
        xml.push_str("</w:tbl>");

        xml
    }

    fn escape_xml(&self, text: &str) -> String {
        text.replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace("\"", "&quot;")
            .replace("'", "&apos;")
    }

    /// 从JSON数组创建表格
    ///
    /// # 参数
    /// * `json_str` - JSON字符串,应该是包含对象的数组
    /// * `with_headers` - 是否将第一行作为表头
    ///
    /// # 返回
    /// * `Result<Self>` - 创建好的表格或错误
    pub fn from_json(data: &Value, with_headers: bool) -> Result<Self> {
        if !data.is_array() {
            return Err(anyhow::anyhow!("JSON数据必须是数组"));
        }

        let array = data.as_array().unwrap(); // 已经检查过是数组,所以可以安全unwrap
        if array.is_empty() {
            return Ok(Self::new());
        }

        let mut table = Self::new();

        // 如果第一个元素是对象,则提取其键作为表头
        if let Some(first_item) = array.first() {
            if first_item.is_object() {
                let obj = first_item.as_object().unwrap();

                // 创建表头行
                if with_headers {
                    let mut header_row = TableRow::new();
                    for key in obj.keys() {
                        header_row.add_cell(TableCell::header(key));
                    }
                    table.add_row(header_row);
                }

                // 为每个数组项创建数据行
                for item in array {
                    if let Some(obj) = item.as_object() {
                        let mut row = TableRow::new();

                        // 确保按照表头的顺序添加单元格
                        if let Some(first_obj) = array.first().and_then(|v| v.as_object()) {
                            for key in first_obj.keys() {
                                if let Some(value) = obj.get(key) {
                                    let cell_value = match value {
                                        Value::Null => "".to_string(),
                                        Value::Bool(b) => b.to_string(),
                                        Value::Number(n) => n.to_string(),
                                        Value::String(s) => s.clone(),
                                        Value::Array(_) | Value::Object(_) => {
                                            serde_json::to_string(value)
                                                .unwrap_or_else(|_| "".to_string())
                                        }
                                    };
                                    row.add_cell(TableCell::new(&cell_value));
                                } else {
                                    row.add_cell(TableCell::new(""));
                                }
                            }
                        }

                        table.add_row(row);
                    }
                }
            } else if first_item.is_array() {
                // 处理数组的数组情况

                // 如果with_headers为true,将第一行作为表头
                let start_index = if with_headers { 1 } else { 0 };

                if with_headers && array.len() > 0 {
                    if let Some(header_array) = array[0].as_array() {
                        let mut header_row = TableRow::new();
                        for header in header_array {
                            let header_text = match header {
                                Value::String(s) => s.clone(),
                                _ => header.to_string(),
                            };
                            header_row.add_cell(TableCell::header(&header_text));
                        }
                        table.add_row(header_row);
                    }
                }

                // 处理数据行
                for i in start_index..array.len() {
                    if let Some(row_array) = array[i].as_array() {
                        let mut row = TableRow::new();
                        for cell in row_array {
                            let cell_value = match cell {
                                Value::Null => "".to_string(),
                                Value::Bool(b) => b.to_string(),
                                Value::Number(n) => n.to_string(),
                                Value::String(s) => s.clone(),
                                Value::Array(_) | Value::Object(_) => {
                                    serde_json::to_string(cell).unwrap_or_else(|_| "".to_string())
                                }
                            };
                            row.add_cell(TableCell::new(&cell_value));
                        }
                        table.add_row(row);
                    }
                }
            }
        }

        Ok(table)
    }
}