k2f_docx 0.2.3

K2F lock → DOCX exporter
Documentation
use super::cell::cell_borders;
use crate::ir::{BorderStroke, CellBorders, TableBox, TableCell, TextAlign, TextBox};
use crate::ooxml;
use crate::xml::{escape_xml, word_hex_color};
use k2f_core::Border;
use std::collections::BTreeMap;

pub fn table_cell_wml(align: TextAlign, border: Option<&Border>) -> String {
    let cell = TableCell {
        node_id: "cell".into(),
        width_twips: 1440,
        runs: Vec::new(),
        align,
        fill_hex: None,
        preserve_whitespace: false,
        borders: cell_borders(border).unwrap_or_default(),
        vert_center: false,
        line_twips: None,
    };
    format!(
        r#"<root xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
{}</root>"#,
        tc_xml(&cell, 1440, &BTreeMap::new())
    )
}

pub(crate) fn table_anchor(
    tbl: &TableBox,
    doc_pr_id: u32,
    hyperlink_rids: &BTreeMap<String, String>,
) -> String {
    ooxml::wp_anchor(
        tbl.x_emu,
        tbl.y_emu,
        tbl.cx_emu,
        tbl.cy_emu,
        tbl.relative_height,
        false,
        doc_pr_id,
        &tbl.node_id,
        "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
        &table_wsp_xml(tbl, hyperlink_rids),
    )
}

fn table_wsp_xml(tbl: &TableBox, hyperlink_rids: &BTreeMap<String, String>) -> String {
    let fill_hex = word_hex_color(tbl.fill_hex.as_deref().unwrap_or("FFFFFE"));
    format!(
        r#"                <wps:wsp>
                  <wps:cNvSpPr txBox="1"/>
                  <wps:spPr>
                    <a:xfrm>
                      <a:off x="0" y="0"/>
                      <a:ext cx="{cx}" cy="{cy}"/>
                    </a:xfrm>
                    <a:prstGeom prst="rect">
                      <a:avLst/>
                    </a:prstGeom>
                    <a:solidFill>
                      <a:srgbClr val="{fill_hex}"/>
                    </a:solidFill>
                    <a:ln>
                      <a:noFill/>
                    </a:ln>
                  </wps:spPr>
                  <wps:txbx>
                    <w:txbxContent>
{tbl_xml}                      <w:p/>
                    </w:txbxContent>
                  </wps:txbx>
                  <wps:bodyPr wrap="square" lIns="0" tIns="0" rIns="0" bIns="0" anchor="t">
                    <a:noAutofit/>
                  </wps:bodyPr>
                </wps:wsp>
"#,
        cx = tbl.cx_emu,
        cy = tbl.cy_emu,
        tbl_xml = tbl_xml(tbl, hyperlink_rids),
    )
}

fn tbl_xml(tbl: &TableBox, hyperlink_rids: &BTreeMap<String, String>) -> String {
    let mut grid = String::new();
    for w in &tbl.col_widths_twips {
        grid.push_str(&format!(
            "                          <w:gridCol w:w=\"{w}\"/>\n"
        ));
    }
    let mut rows = String::new();
    for row in &tbl.rows {
        rows.push_str(&tr_xml(row, &tbl.col_widths_twips, hyperlink_rids));
    }
    format!(
        r#"                      <w:tbl>
                        <w:tblPr>
                          <w:tblStyle w:val="TableNormal"/>
                          <w:tblW w:w="{w}" w:type="dxa"/>
                          <w:tblLayout w:type="fixed"/>
                          <w:tblLook w:val="0000" w:firstRow="0" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="1" w:noVBand="1"/>
                          <w:tblCellMar>
                            <w:top w:w="0" w:type="dxa"/>
                            <w:left w:w="0" w:type="dxa"/>
                            <w:bottom w:w="0" w:type="dxa"/>
                            <w:right w:w="0" w:type="dxa"/>
                          </w:tblCellMar>
                        </w:tblPr>
                        <w:tblGrid>
{grid}                        </w:tblGrid>
{rows}                      </w:tbl>
"#,
        w = tbl.width_twips,
    )
}

fn tr_xml(
    row: &crate::ir::TableRow,
    col_widths: &[i64],
    hyperlink_rids: &BTreeMap<String, String>,
) -> String {
    let mut cells = String::new();
    for (i, cell) in row.cells.iter().enumerate() {
        let w = col_widths.get(i).copied().unwrap_or(cell.width_twips);
        cells.push_str(&tc_xml(cell, w, hyperlink_rids));
    }
    format!(
        r#"                        <w:tr>
                          <w:trPr>
                            <w:trHeight w:val="{h}" w:hRule="atLeast"/>
                          </w:trPr>
{cells}                        </w:tr>
"#,
        h = row.height_twips,
    )
}

fn tc_xml(cell: &TableCell, width_twips: i64, hyperlink_rids: &BTreeMap<String, String>) -> String {
    let shd = match &cell.fill_hex {
        Some(hex) => format!(
            "                              <w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"{}\"/>\n",
            word_hex_color(hex)
        ),
        None => String::new(),
    };
    let valign = if cell.vert_center {
        "                              <w:vAlign w:val=\"center\"/>\n"
    } else {
        ""
    };
    let dummy = TextBox {
        node_id: cell.node_id.clone(),
        x_emu: 0,
        y_emu: 0,
        cx_emu: 0,
        cy_emu: 0,
        runs: cell.runs.clone(),
        align: cell.align,
        bullet: false,
        numbered: false,
        ilvl: 0,
        l_ins_emu: 0,
        t_ins_emu: 0,
        r_ins_emu: 0,
        b_ins_emu: 0,
        line_twips: cell.line_twips,
        vert_center: cell.vert_center,
        preserve_whitespace: cell.preserve_whitespace,
        relative_height: 0,
        fill_hex: None,
        fill_alpha: 255,
        wrap: true,
        corner_emu: 0,
    };
    format!(
        r#"                          <w:tc>
                            <w:tcPr>
                              <w:tcW w:w="{width_twips}" w:type="dxa"/>
{borders}{shd}{valign}                            </w:tcPr>
{paras}                          </w:tc>
"#,
        borders = tc_borders_xml(&cell.borders),
        paras = ooxml::txbx_paragraphs(&dummy, hyperlink_rids),
    )
}

fn tc_borders_xml(b: &CellBorders) -> String {
    format!(
        r#"                              <w:tcBorders>
                                {top}
                                {left}
                                {bottom}
                                {right}
                              </w:tcBorders>
"#,
        top = edge_xml("top", b.top.as_ref()),
        left = edge_xml("left", b.left.as_ref()),
        bottom = edge_xml("bottom", b.bottom.as_ref()),
        right = edge_xml("right", b.right.as_ref()),
    )
}

fn edge_xml(name: &str, stroke: Option<&BorderStroke>) -> String {
    match stroke {
        Some(s) => format!(
            r#"<w:{name} w:val="{val}" w:sz="{sz}" w:space="0" w:color="{color}"/>"#,
            val = s.val,
            sz = s.sz,
            color = escape_xml(&s.color_hex),
        ),
        None => format!(r#"<w:{name} w:val="nil"/>"#),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::TextAlign;

    #[test]
    fn v_align_center_emits_w_v_align() {
        let mut cell = TableCell {
            node_id: "c".into(),
            width_twips: 1440,
            runs: Vec::new(),
            align: TextAlign::Left,
            fill_hex: None,
            preserve_whitespace: false,
            borders: CellBorders::default(),
            vert_center: true,
            line_twips: None,
        };
        let xml = tc_xml(&cell, 1440, &BTreeMap::new());
        assert!(
            xml.contains(r#"<w:vAlign w:val="center"/>"#),
            "centered lock cell must emit w:vAlign, got {xml}"
        );
        cell.vert_center = false;
        let top = tc_xml(&cell, 1440, &BTreeMap::new());
        assert!(
            !top.contains("vAlign"),
            "top-aligned cell must not emit vAlign, got {top}"
        );
    }
}