spb/firc0001_uqo
Public
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# ---- WP 28 shims ----------------------------------------------------------57# TikZ pipeline figure -> pre-rendered PNG58tex = re.sub(r'\\begin\{tikzpicture\}.*?\\end\{tikzpicture\}', r'\\includegraphics[width=\\textwidth]{figures/fig_pipeline.png}', tex, flags=re.S)59# PDF figures -> PNG (pandoc/docx cannot embed PDF)60tex = re.sub(r'\\includegraphics(\[[^\]]*\])?\{figures/([^}]+)\.pdf\}', r'\\includegraphics\1{figures/\2.png}', tex)61# \adjustbox{max width=...}{...} wrapper -> content only62def strip_adjustbox(t):63 out = []; pos = 064 while True:65 m = re.search(r'\\adjustbox\{[^{}]*\}\{', t[pos:])66 if not m: break67 start = pos + m.start(); open_ = pos + m.end() - 168 content, end = brace_arg(t, open_)69 out.append(t[pos:start]); out.append(content); pos = end70 out.append(t[pos:]); return "".join(out)71tex = strip_adjustbox(tex)72# threeparttable / tablenotes -> plain notes paragraph73tex = re.sub(r'\\(begin|end)\{threeparttable\}', '', tex)74tex = re.sub(r'\\begin\{tablenotes\}\\footnotesize\s*\\item\s*(.*?)\\end\{tablenotes\}', lambda m: '\\par\\textit{Notes:} ' + m.group(1).strip(), tex, flags=re.S)75# prompt block (quote + obeylines) -> verbatim76tex = re.sub(r'\\begin\{quote\}\\footnotesize\\ttfamily.*?\\end\{quote\}', lambda m: (PAPER / 'tables' / 'prompt_verbatim.tex').read_text(), tex, flags=re.S)77tex = re.sub(r'\\input\{tables/prompt\}', lambda m: (PAPER / 'tables' / 'prompt_verbatim.tex').read_text(), tex)78# tabularx -> tabular (pandoc handles tabular more reliably); X -> l79def fix_tabularx(m):80 spec = m.group(1).replace('X', 'l')81 return '\\begin{tabular}{' + spec + '}'82tex = re.sub(r'\\begin\{tabularx\}\{\\textwidth\}\{([^}]*)\}', fix_tabularx, tex)83tex = tex.replace('\\end{tabularx}', '\\end{tabular}')84tex = tex.replace('\\allowbreak', '')85tex = re.sub(r'\\setlength\{\\emergencystretch\}\{[^}]*\}', '', tex)86tex = re.sub(r'\\begin\{tabular\}\{lr@\{\\hspace\{2em\}\}lr@\{\\hspace\{2em\}\}lr\}', r'\\begin{tabular}{lrlrlr}', tex)87tex = re.sub(r'\\begin\{tabular\}\{llllp\{[^}]*\}\}', r'\\begin{tabular}{lllll}', tex)8889# titlepage: pandoc cannot evaluate \ifx -> keep the \else branch only90tex = re.sub(r'\\ifx\\WPsubtitle\\empty\\else(.*?)\\fi', r'\1', tex, flags=re.S)9192# appendix environment / counters that pandoc does not understand93tex = re.sub(r'\\(begin|end)\{appendices\}', '', tex)94tex = re.sub(r'\\renewcommand\{\\the(table|figure)\}\{[^{}]*(\{[^{}]*\})*[^{}]*\}', '', tex)95tex = re.sub(r'\\setcounter\{(table|figure)\}\{0\}', '', tex)9697# ---------------------------------------------------------------- headings98pos = 0; out = []; ti = 099pat = re.compile(r'\\(section|subsection|subsubsection)\{')100while True:101 m = pat.search(tex, pos)102 if not m: break103 title, end = brace_arg(tex, m.end()-1)104 out.append(tex[pos:m.start()])105 lvl = m.group(1)106 if ti < len(toc) and toc[ti][0] == lvl:107 num = toc[ti][1]; ti += 1108 if re.sub(r'\W', '', toc[ti-1][2])[:12] != re.sub(r'\W', '', title)[:12]:109 warn("heading mismatch:", repr(toc[ti-1][2][:40]), "vs", repr(title[:40]))110 out.append(f"\\{lvl}{{{num}~{title}}}")111 else:112 warn("unnumbered heading kept:", lvl, title[:40])113 out.append(tex[m.start():end])114 pos = end115out.append(tex[pos:]); tex = "".join(out)116if ti != len(toc): warn(f"only {ti}/{len(toc)} toc entries consumed")117118# ---------------------------------------------------------------- floats: caption prefix119def fix_float(m):120 kind, body = m.group(1), m.group(2)121 labs = [l for l in re.findall(r'\\label\{([^}]+)\}', body) if l in labels]122 if not labs:123 warn("float without known label", body[:60]); return m.group(0)124 num = labels[labs[0]]125 word = 'Table' if kind == 'table' else 'Figure'126 cm = re.search(r'\\caption(\[[^\]]*\])?\{', body)127 if not cm: return m.group(0)128 cap, end = brace_arg(body, cm.end()-1)129 body = body[:cm.start()] + f"\\caption{{{word} {num}: {cap}}}" + body[end:]130 return f"\\begin{{{kind}}}{body}\\end{{{kind}}}"131tex = re.sub(r'\\begin\{(table|figure)\}(.*?)\\end\{\1\}', fix_float, tex, flags=re.S)132133# ---------------------------------------------------------------- equations: number tag134def fix_eq(m):135 body = m.group(1)136 lm = re.search(r'\\label\{([^}]+)\}', body)137 if lm and lm.group(1) in labels:138 body = body.replace(lm.group(0), '')139 body = body.rstrip() + f"\\qquad\\text{{({labels[lm.group(1)]})}}\n"140 return "\\begin{equation}" + body + "\\end{equation}"141tex = re.sub(r'\\begin\{equation\}(.*?)\\end\{equation\}', fix_eq, tex, flags=re.S)142143# ---------------------------------------------------------------- cross-references144missing = set()145def ref(m):146 k = m.group(2)147 if k not in labels:148 missing.add(k); return '??'149 return f"({labels[k]})" if m.group(1) == 'eqref' else labels[k]150tex = re.sub(r'\\(eqref|ref)\{([^}]+)\}', ref, tex)151if missing: warn("unresolved labels:", sorted(missing))152tex = re.sub(r'\\label\{[^}]+\}', '', tex)153154# booktabs partial rules have no Word equivalent and leak as text155tex = re.sub(r'\\cmidrule(\([^)]*\))?\{[^}]*\}', '', tex)156157# ---------------------------------------------------------------- pandoc shims158shims = r"""159\newcommand{\num}[1]{#1}160\renewcommand{\tablelegend}[1]{\par\textit{Notes:} #1}161\renewcommand{\tstat}[1]{(\textit{t}=#1)}162\newcommand{\textperiodcentered}{·}163"""164tex = tex.replace("\\begin{document}", shims + "\\begin{document}", 1)165tex = re.sub(r'(\\newcommand\{\\WPauthor\}\{[^}]*)\}', lambda m: m.group(1).replace('\\quad', '~~~~') + '}', tex)166tex = tex.replace("\\newcommand{\\textperiodcentered}{·}", "\\providecommand{\\textperiodcentered}{·}")167OUT.write_text(tex)168print("wrote", OUT, len(tex), "chars")169