1"""Flatten a LaTeX working paper and resolve numbering from main.aux so that2pandoc produces a Word file with correct section/table/figure/equation numbers."""3import re, sys, pathlib45PAPER = pathlib.Path(sys.argv[1]).resolve()6OUT = pathlib.Path(sys.argv[2])7warn = lambda *a: print("WARN:", *a, file=sys.stderr)89def strip_comments(txt):10 out = []11 for line in txt.splitlines():12 if re.match(r'^\s*%', line):13 continue14 # drop trailing unescaped % comments15 line = re.sub(r'(?<!\\)%.*$', '', line)16 out.append(line)17 return "\n".join(out) + "\n"1819def inline(path):20 txt = strip_comments(path.read_text())21 def rep(m):22 name = m.group(1)23 p = PAPER / (name if name.endswith('.tex') else name + '.tex')24 if not p.exists():25 warn("missing input", p); return m.group(0)26 return inline(p)27 return re.sub(r'\\input\{([^}]+)\}', rep, txt)2829def brace_arg(s, i):30 """s[i] == '{' -> return (content, index after closing brace)"""31 assert s[i] == '{', s[i:i+20]32 depth = 033 for j in range(i, len(s)):34 if s[j] == '{' and (j == 0 or s[j-1] != '\\'): depth += 135 elif s[j] == '}' and s[j-1] != '\\':36 depth -= 137 if depth == 0:38 return s[i+1:j], j+139 raise ValueError("unbalanced braces")4041# ---------------------------------------------------------------- aux42aux = (PAPER / 'main.aux').read_text()43labels = {}44for m in re.finditer(r'\\newlabel\{([^}]+)\}\{', aux):45 outer, _ = brace_arg(aux, m.end()-1)46 num, _ = brace_arg(outer, 0)47 labels[m.group(1)] = num.strip()48toc = []49for m in re.finditer(r'\\contentsline \{(section|subsection|subsubsection)\}\{\\numberline \{([^}]*)\}(.*?)\}\{\d+\}\{', aux):50 toc.append((m.group(1), m.group(2), m.group(3)))51print(f"aux: {len(labels)} labels, {len(toc)} numbered headings")5253# ---------------------------------------------------------------- flatten54tex = inline(PAPER / 'main.tex')5556# titlepage: pandoc cannot evaluate \ifx -> keep the \else branch only57tex = re.sub(r'\\ifx\\WPsubtitle\\empty\\else(.*?)\\fi', r'\1', tex, flags=re.S)5859# appendix environment / counters that pandoc does not understand60tex = re.sub(r'\\(begin|end)\{appendices\}', '', tex)61tex = re.sub(r'\\renewcommand\{\\the(table|figure)\}\{[^{}]*(\{[^{}]*\})*[^{}]*\}', '', tex)62tex = re.sub(r'\\setcounter\{(table|figure)\}\{0\}', '', tex)6364# ---------------------------------------------------------------- headings65pos = 0; out = []; ti = 066pat = re.compile(r'\\(section|subsection|subsubsection)\{')67while True:68 m = pat.search(tex, pos)69 if not m: break70 title, end = brace_arg(tex, m.end()-1)71 out.append(tex[pos:m.start()])72 lvl = m.group(1)73 if ti < len(toc) and toc[ti][0] == lvl:74 num = toc[ti][1]; ti += 175 if re.sub(r'\W', '', toc[ti-1][2])[:12] != re.sub(r'\W', '', title)[:12]:76 warn("heading mismatch:", repr(toc[ti-1][2][:40]), "vs", repr(title[:40]))77 out.append(f"\\{lvl}{{{num}~{title}}}")78 else:79 warn("unnumbered heading kept:", lvl, title[:40])80 out.append(tex[m.start():end])81 pos = end82out.append(tex[pos:]); tex = "".join(out)83if ti != len(toc): warn(f"only {ti}/{len(toc)} toc entries consumed")8485# ---------------------------------------------------------------- floats: caption prefix86def fix_float(m):87 kind, body = m.group(1), m.group(2)88 labs = [l for l in re.findall(r'\\label\{([^}]+)\}', body) if l in labels]89 if not labs:90 warn("float without known label", body[:60]); return m.group(0)91 num = labels[labs[0]]92 word = 'Table' if kind == 'table' else 'Figure'93 cm = re.search(r'\\caption(\[[^\]]*\])?\{', body)94 if not cm: return m.group(0)95 cap, end = brace_arg(body, cm.end()-1)96 body = body[:cm.start()] + f"\\caption{{{word} {num}: {cap}}}" + body[end:]97 return f"\\begin{{{kind}}}{body}\\end{{{kind}}}"98tex = re.sub(r'\\begin\{(table|figure)\}(.*?)\\end\{\1\}', fix_float, tex, flags=re.S)99100# ---------------------------------------------------------------- equations: number tag101def fix_eq(m):102 body = m.group(1)103 lm = re.search(r'\\label\{([^}]+)\}', body)104 if lm and lm.group(1) in labels:105 body = body.replace(lm.group(0), '')106 body = body.rstrip() + f"\\qquad\\text{{({labels[lm.group(1)]})}}\n"107 return "\\begin{equation}" + body + "\\end{equation}"108tex = re.sub(r'\\begin\{equation\}(.*?)\\end\{equation\}', fix_eq, tex, flags=re.S)109110# ---------------------------------------------------------------- cross-references111missing = set()112def ref(m):113 k = m.group(2)114 if k not in labels:115 missing.add(k); return '??'116 return f"({labels[k]})" if m.group(1) == 'eqref' else labels[k]117tex = re.sub(r'\\(eqref|ref)\{([^}]+)\}', ref, tex)118if missing: warn("unresolved labels:", sorted(missing))119tex = re.sub(r'\\label\{[^}]+\}', '', tex)120121# booktabs partial rules have no Word equivalent and leak as text122tex = re.sub(r'\\cmidrule(\([^)]*\))?\{[^}]*\}', '', tex)123124# ---------------------------------------------------------------- pandoc shims125shims = r"""126\newcommand{\num}[1]{#1}127\renewcommand{\tablelegend}[1]{\par\textit{Notes:} #1}128\renewcommand{\tstat}[1]{(\textit{t}=#1)}129\newcommand{\textperiodcentered}{·}130"""131tex = tex.replace("\\begin{document}", shims + "\\begin{document}", 1)132tex = re.sub(r'(\\newcommand\{\\WPauthor\}\{[^}]*)\}', lambda m: m.group(1).replace('\\quad', '~~~~') + '}', tex)133OUT.write_text(tex)134print("wrote", OUT, len(tex), "chars")135