@@ -0,0 +1,140 @@
|
||||
"""DOCX table → markdown rows with ^ / > merge markers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from docx.oxml.ns import qn
|
||||
from docx.table import Table as DocxTable
|
||||
|
||||
|
||||
def _cell_v_merge(tc) -> str:
|
||||
"""Return none | restart | continue for vertical merge."""
|
||||
tcPr = tc.tcPr
|
||||
if tcPr is None:
|
||||
return "none"
|
||||
vMerge = tcPr.find(qn("w:vMerge"))
|
||||
if vMerge is None:
|
||||
return "none"
|
||||
val = vMerge.get(qn("w:val"))
|
||||
if val in (None, "continue"):
|
||||
return "continue"
|
||||
return "restart"
|
||||
|
||||
|
||||
def _cell_grid_span(tc) -> int:
|
||||
tcPr = tc.tcPr
|
||||
if tcPr is None:
|
||||
return 1
|
||||
span = tcPr.find(qn("w:gridSpan"))
|
||||
if span is None:
|
||||
return 1
|
||||
try:
|
||||
return max(1, int(span.get(qn("w:val")) or "1"))
|
||||
except ValueError:
|
||||
return 1
|
||||
|
||||
|
||||
def _cell_text(cell) -> str:
|
||||
from .text import paragraph_plain
|
||||
|
||||
parts: list[str] = []
|
||||
for p in cell.paragraphs:
|
||||
t = paragraph_plain(p)
|
||||
if t:
|
||||
parts.append(t)
|
||||
return " ".join(parts).replace("|", "\\|")
|
||||
|
||||
|
||||
def table_to_grid(docx_table: DocxTable) -> tuple[list[list[str]], list[list[tuple[str, str]]]]:
|
||||
"""
|
||||
Expand a Word table into a rectangular grid.
|
||||
|
||||
merges[r][c] = (v, h) where v/h in {none, restart, continue}.
|
||||
Horizontally spanned continue cells are filled with '>' semantics.
|
||||
Vertically continued cells use '^'.
|
||||
"""
|
||||
rows_xml = docx_table._tbl.tr_lst
|
||||
# First pass: determine max columns from grid spans
|
||||
max_cols = 0
|
||||
row_infos: list[list[tuple[object, int, str]]] = [] # (tc, span, vmerge)
|
||||
for tr in rows_xml:
|
||||
info: list[tuple[object, int, str]] = []
|
||||
col = 0
|
||||
for tc in tr.tc_lst:
|
||||
span = _cell_grid_span(tc)
|
||||
v = _cell_v_merge(tc)
|
||||
info.append((tc, span, v))
|
||||
col += span
|
||||
max_cols = max(max_cols, col)
|
||||
row_infos.append(info)
|
||||
|
||||
if max_cols == 0:
|
||||
return [], []
|
||||
|
||||
texts: list[list[str]] = [["-" for _ in range(max_cols)] for _ in range(len(row_infos))]
|
||||
merges: list[list[tuple[str, str]]] = [
|
||||
[("none", "none") for _ in range(max_cols)] for _ in range(len(row_infos))
|
||||
]
|
||||
|
||||
# Map tc elements to python-docx cells by index
|
||||
for r, info in enumerate(row_infos):
|
||||
c = 0
|
||||
cell_idx = 0
|
||||
for tc, span, v in info:
|
||||
try:
|
||||
cell = docx_table.rows[r].cells[cell_idx]
|
||||
text = _cell_text(cell)
|
||||
except Exception:
|
||||
text = ""
|
||||
cell_idx += 1
|
||||
|
||||
if v == "continue":
|
||||
merges[r][c] = ("continue", "none")
|
||||
texts[r][c] = "^"
|
||||
else:
|
||||
merges[r][c] = ("restart" if v == "restart" else "none", "none" if span == 1 else "restart")
|
||||
texts[r][c] = text if text else " "
|
||||
for k in range(1, span):
|
||||
if c + k < max_cols:
|
||||
merges[r][c + k] = ("none", "continue")
|
||||
texts[r][c + k] = ">"
|
||||
c += span
|
||||
|
||||
return texts, merges
|
||||
|
||||
|
||||
def is_formula_table(docx_table: DocxTable) -> bool:
|
||||
"""Heuristic: 1×2 table with Formula Content / Formula Numbering styles."""
|
||||
try:
|
||||
if len(docx_table.rows) != 1 or len(docx_table.columns) != 2:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
styles: list[str] = []
|
||||
for cell in docx_table.rows[0].cells:
|
||||
for p in cell.paragraphs:
|
||||
name = (p.style.name if p.style is not None else "") or ""
|
||||
styles.append(name)
|
||||
joined = " ".join(styles).lower()
|
||||
return "formula" in joined
|
||||
|
||||
|
||||
def formula_table_parts(docx_table: DocxTable) -> tuple[object | None, str | None]:
|
||||
"""Return (omml_element_or_None, number_text_or_None) from a formula table."""
|
||||
from .omml import find_omml, omml_to_latex
|
||||
from .text import paragraph_plain
|
||||
|
||||
left = docx_table.rows[0].cells[0]
|
||||
right = docx_table.rows[0].cells[1]
|
||||
latex = ""
|
||||
for p in left.paragraphs:
|
||||
for om in find_omml(p._element):
|
||||
latex = omml_to_latex(om)
|
||||
if latex:
|
||||
break
|
||||
if latex:
|
||||
break
|
||||
if not latex:
|
||||
latex = " ".join(paragraph_plain(p) for p in left.paragraphs).strip()
|
||||
number = " ".join(paragraph_plain(p) for p in right.paragraphs).strip()
|
||||
number = number.strip("() ").strip() or None
|
||||
return latex, number
|
||||
Reference in New Issue
Block a user