from __future__ import annotations
import os
import re
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "DRAFT.md")
OUT = os.path.join(HERE, "paper.tex")
PREAMBLE = r"""\documentclass[11pt]{article}
\usepackage[margin=1in]{geometry}
\usepackage{amssymb}
\usepackage{booktabs}
\usepackage{array}
\usepackage{longtable}
\usepackage{enumitem}
\usepackage{fancyvrb}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{microtype}
\usepackage[hidelinks]{hyperref}
\setlength{\parskip}{0.5em}
\setlength{\parindent}{0pt}
\newcommand{\code}[1]{\texttt{#1}}
% Tighten tables and allow them to break/scale.
\renewcommand{\arraystretch}{1.15}
\title{@@TITLE@@}
\author{[author]\\\normalsize (preprint --- authorship to be set before submission)}
\date{\today}
\begin{document}
\maketitle
"""
UNICODE = {
"→": r"$\rightarrow$", "≤": r"$\le$", "≥": r"$\ge$", "∉": r"$\notin$", "×": r"$\times$", "—": "---", "–": "--", "§": r"\S{}", "…": r"\ldots{}", "✓": r"$\checkmark$", "±": r"$\pm$", "≈": r"$\approx$", "≥": r"$\ge$",
"’": "'", "‘": "`", "“": "``", "”": "''", }
SPECIALS = {
"\\": r"\textbackslash{}",
"&": r"\&",
"%": r"\%",
"#": r"\#",
"_": r"\_",
"{": r"\{",
"}": r"\}",
"$": r"\$",
"~": r"\textasciitilde{}",
"^": r"\textasciicircum{}",
"|": r"\textbar{}", }
def esc(s: str) -> str:
out = []
for ch in s:
if ch in SPECIALS:
out.append(SPECIALS[ch])
elif ch in UNICODE:
out.append(UNICODE[ch])
else:
out.append(ch)
return "".join(out)
def inline(s: str) -> str:
codes: list[str] = []
def stash(m: re.Match) -> str:
codes.append(m.group(1))
return f"\x00{len(codes) - 1}\x00"
s = re.sub(r"`([^`]+)`", stash, s)
s = re.sub(r"\*\*([^*]+)\*\*", lambda m: f"\x01B\x01{m.group(1)}\x01b\x01", s)
s = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", lambda m: f"\x01I\x01{m.group(1)}\x01i\x01", s)
s = esc(s)
s = s.replace("\x01B\x01", r"\textbf{").replace("\x01b\x01", "}")
s = s.replace("\x01I\x01", r"\emph{").replace("\x01i\x01", "}")
def restore(m: re.Match) -> str:
return r"\code{" + esc(codes[int(m.group(1))]) + "}"
s = re.sub(r"\x00(\d+)\x00", restore, s)
return s
def convert(md: str) -> str:
lines = md.split("\n")
out: list[str] = []
i = 0
n = len(lines)
title = "Purpose-sized languages"
list_stack: list[str] = []
def close_lists(to_depth: int = 0) -> None:
while len(list_stack) > to_depth:
out.append(f"\\end{{{list_stack.pop()}}}")
while i < n:
line = lines[i]
if line.startswith("```"):
close_lists()
out.append(r"\begin{Verbatim}[fontsize=\small,frame=single,samepage=false]")
i += 1
while i < n and not lines[i].startswith("```"):
out.append(lines[i])
i += 1
out.append(r"\end{Verbatim}")
i += 1
continue
if line.lstrip().startswith("|"):
close_lists()
tbl: list[str] = []
while i < n and lines[i].lstrip().startswith("|"):
tbl.append(lines[i].strip())
i += 1
out.append(render_table(tbl))
continue
m = re.match(r"^(#{1,3})\s+(.*)$", line)
if m:
close_lists()
level, text = len(m.group(1)), m.group(2).strip()
if level == 1:
title = text elif level == 2:
out.append(r"\section*{" + inline(text) + "}")
else:
out.append(r"\subsection*{" + inline(text) + "}")
i += 1
continue
if line.strip() == "---":
close_lists()
out.append(r"\bigskip\hrule\bigskip")
i += 1
continue
lm = re.match(r"^(\s*)([-*]|\d+\.)\s+(.*)$", line)
if lm:
indent, marker, text = lm.group(1), lm.group(2), lm.group(3)
depth = len(indent) // 2 + 1 kind = "enumerate" if marker[0].isdigit() else "itemize"
while len(list_stack) > depth:
out.append(f"\\end{{{list_stack.pop()}}}")
if len(list_stack) == depth and list_stack and list_stack[-1] != kind:
out.append(f"\\end{{{list_stack.pop()}}}")
while len(list_stack) < depth:
out.append(f"\\begin{{{kind}}}[leftmargin=*]")
list_stack.append(kind)
out.append(r"\item " + inline(text))
i += 1
continue
if line.strip() == "":
close_lists()
out.append("")
i += 1
continue
close_lists()
out.append(inline(line))
i += 1
close_lists()
body = "\n".join(out)
return PREAMBLE.replace("@@TITLE@@", esc(title)) + body + "\n\\end{document}\n"
def render_table(rows: list[str]) -> str:
def cells(r: str) -> list[str]:
r = r.strip().strip("|")
return [c.strip().replace(r"\|", "|") for c in re.split(r"(?<!\\)\|", r)]
header = cells(rows[0])
ncol = len(header)
aligns = []
for spec in cells(rows[1]) if len(rows) > 1 else []:
if spec.startswith(":") and spec.endswith(":"):
aligns.append("c")
elif spec.endswith(":"):
aligns.append("r")
else:
aligns.append("l")
while len(aligns) < ncol:
aligns.append("l")
colspec = " ".join(f"p{{{0.9 / ncol:.3f}\\linewidth}}" for _ in range(ncol))
body_rows = rows[2:] if len(rows) > 2 else []
out = [r"\begingroup\small", r"\begin{longtable}{" + colspec + "}", r"\toprule"]
out.append(" & ".join(r"\textbf{" + inline(c) + "}" for c in header) + r" \\")
out.append(r"\midrule\endhead")
for r in body_rows:
c = cells(r)
c += [""] * (ncol - len(c))
out.append(" & ".join(inline(x) for x in c[:ncol]) + r" \\")
out.append(r"\bottomrule")
out.append(r"\end{longtable}\endgroup")
return "\n".join(out)
if __name__ == "__main__":
with open(SRC, encoding="utf-8") as f:
md = f.read()
tex = convert(md)
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(tex)
print(f"wrote {OUT} ({tex.count(chr(10))} lines)")