from copy import deepcopy from docx.oxml import CT_Tbl from docx.oxml.ns import qn from docx.shared import Length, Parented from docx.table import Table, _Row, _Cell from md2gost.util import create_element def _twips(value) -> int: """Length or EMU-int → twips. python-docx ``Length + Length`` returns a bare ``int`` in EMUs, not twips. """ if hasattr(value, "twips"): return int(value.twips) # bare int from Length arithmetic is EMU (1 twip = 635 EMU) return int(value) // 635 __all__ = [ "create_table", "create_table_row", "create_table_cell", "apply_cell_merge", ] def _set_table_width(table: Table, width: Length) -> None: """Force fixed table width (CT_Tbl.new_tbl leaves tblW auto/0 → Word shrinks table).""" tbl = table._tbl tblPr = tbl.tblPr if tblPr is None: tblPr = create_element("w:tblPr") tbl.insert(0, tblPr) for el in tblPr.findall(qn("w:tblW")): tblPr.remove(el) tblPr.append(create_element("w:tblW", { "w:type": "dxa", "w:w": str(_twips(width)), })) for el in tblPr.findall(qn("w:tblLayout")): tblPr.remove(el) tblPr.append(create_element("w:tblLayout", {"w:type": "fixed"})) for el in tblPr.findall(qn("w:tblInd")): tblPr.remove(el) tblPr.append(create_element("w:tblInd", { "w:type": "dxa", "w:w": "0", })) def _sync_grid_cols(table: Table, cols: int, width: Length) -> None: tbl = table._tbl grid = tbl.tblGrid if grid is None: grid = create_element("w:tblGrid") tbl.insert(list(tbl).index(tbl.tblPr) + 1 if tbl.tblPr is not None else 0, grid) for el in list(grid.findall(qn("w:gridCol"))): grid.remove(el) col_w = max(_twips(width) // max(cols, 1), 1) for _ in range(cols): grid.append(create_element("w:gridCol", {"w:w": str(col_w)})) def create_table(parent: Parented, rows: int, cols, width: Length, style="Table Grid"): table = Table(CT_Tbl.new_tbl(rows, cols, width), parent) table.style = style _set_table_width(table, width) _sync_grid_cols(table, cols, width) # google docs fix borders = parent.part.styles[style]._element.xpath("w:tblPr/w:tblBorders") if borders: tblPr = table._tbl.tblPr for el in tblPr.findall(qn("w:tblBorders")): tblPr.remove(el) tblPr.append(deepcopy(borders[0])) for i in range(rows): for j in range(cols): cell = table.cell(i, j) cell._element.remove(cell.paragraphs[0]._element) table.cell(i, j)._element.tcPr.append(create_element("w:shd", { "w:fill": "auto", "w:val": "clear" })) return table def create_table_row(parent: Table, *, header: bool = False): children = [create_element("w:cantSplit")] if header: children.append(create_element("w:tblHeader")) row = _Row(create_element("w:tr"), parent) row._tr.insert(0, create_element("w:trPr", children)) return row def create_table_cell(parent: _Row, width: Length): cell = _Cell(create_element("w:tc"), parent) cell.width = width return cell def apply_cell_merge(cell: _Cell, v_merge: str | None = None, grid_span: int | None = None) -> None: """Apply Word OXML vertical merge and/or horizontal gridSpan on a cell. v_merge: ``restart`` | ``continue`` | None grid_span: column span (>= 2) or None """ tc = cell._tc tcPr = tc.get_or_add_tcPr() if v_merge == "restart": tcPr.append(create_element("w:vMerge", {"w:val": "restart"})) elif v_merge == "continue": tcPr.append(create_element("w:vMerge")) if grid_span and grid_span > 1: tcPr.append(create_element("w:gridSpan", {"w:val": str(grid_span)}))