SPB Git

spb/firc0001_uqo

Public
7.6 KB · 169 lines python
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
Files · 9aef0752afd87e2743f6f1175bf1d489f07b218b
Menu

Theme