Milestone 0: import zappier billing, Verae middleware, and Zapier research
Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
commit
b4150c8250
1364 changed files with 6814366 additions and 0 deletions
311
research/zapier/scripts/md_to_guide_pdf.py
Normal file
311
research/zapier/scripts/md_to_guide_pdf.py
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Render a Markdown guide to a letter PDF (headings, tables, code, bullets).
|
||||
|
||||
Usage:
|
||||
python3 scripts/md_to_guide_pdf.py INPUT.md OUTPUT.pdf [title]
|
||||
python3 scripts/md_to_guide_pdf.py --concat OUT.pdf TITLE file1.md file2.md ...
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.platypus import (
|
||||
PageBreak,
|
||||
Paragraph,
|
||||
Preformatted,
|
||||
SimpleDocTemplate,
|
||||
Spacer,
|
||||
Table,
|
||||
TableStyle,
|
||||
)
|
||||
|
||||
NAVY = colors.HexColor("#0F2744")
|
||||
TEAL = colors.HexColor("#1A6B6B")
|
||||
SLATE = colors.HexColor("#334155")
|
||||
RULE = colors.HexColor("#CBD5E1")
|
||||
ROW = colors.HexColor("#F1F5F9")
|
||||
CODE_BG = colors.HexColor("#F8FAFC")
|
||||
USABLE = letter[0] - 1.4 * inch
|
||||
|
||||
|
||||
def _styles():
|
||||
base = getSampleStyleSheet()
|
||||
return {
|
||||
"cover_kicker": ParagraphStyle(
|
||||
"k", parent=base["Normal"], fontName="Helvetica", fontSize=10,
|
||||
textColor=TEAL, alignment=TA_CENTER, spaceAfter=8,
|
||||
),
|
||||
"cover_title": ParagraphStyle(
|
||||
"t", parent=base["Title"], fontName="Helvetica-Bold", fontSize=20,
|
||||
leading=26, textColor=NAVY, alignment=TA_CENTER, spaceAfter=10,
|
||||
),
|
||||
"h1": ParagraphStyle(
|
||||
"h1", parent=base["Heading1"], fontName="Helvetica-Bold",
|
||||
fontSize=13.5, leading=17, textColor=NAVY, spaceBefore=12, spaceAfter=6,
|
||||
),
|
||||
"h2": ParagraphStyle(
|
||||
"h2", parent=base["Heading2"], fontName="Helvetica-Bold",
|
||||
fontSize=11.2, leading=14.5, textColor=TEAL, spaceBefore=9, spaceAfter=4,
|
||||
),
|
||||
"h3": ParagraphStyle(
|
||||
"h3", parent=base["Heading3"], fontName="Helvetica-Bold",
|
||||
fontSize=10, leading=13, textColor=NAVY, spaceBefore=7, spaceAfter=3,
|
||||
),
|
||||
"body": ParagraphStyle(
|
||||
"b", parent=base["Normal"], fontName="Helvetica", fontSize=9.1,
|
||||
leading=12.4, textColor=SLATE, alignment=TA_LEFT, spaceAfter=5,
|
||||
),
|
||||
"bullet": ParagraphStyle(
|
||||
"bu", parent=base["Normal"], fontName="Helvetica", fontSize=9.1,
|
||||
leading=12.2, textColor=SLATE, leftIndent=14, firstLineIndent=-10, spaceAfter=2,
|
||||
),
|
||||
"cell": ParagraphStyle(
|
||||
"c", parent=base["Normal"], fontName="Helvetica", fontSize=7.4,
|
||||
leading=10, textColor=SLATE,
|
||||
),
|
||||
"cell_h": ParagraphStyle(
|
||||
"ch", parent=base["Normal"], fontName="Helvetica-Bold", fontSize=7.4,
|
||||
leading=10, textColor=colors.white,
|
||||
),
|
||||
"code": ParagraphStyle(
|
||||
"co", parent=base["Code"], fontName="Courier", fontSize=7.2,
|
||||
leading=9.6, textColor=NAVY, backColor=CODE_BG, leftIndent=4,
|
||||
rightIndent=4, spaceBefore=2, spaceAfter=6,
|
||||
),
|
||||
"caption": ParagraphStyle(
|
||||
"ca", parent=base["Normal"], fontName="Helvetica-Oblique", fontSize=8,
|
||||
leading=11, textColor=colors.HexColor("#64748B"), spaceAfter=8, alignment=TA_CENTER,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
S = _styles()
|
||||
|
||||
|
||||
def _esc(text: str) -> str:
|
||||
return (
|
||||
text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
|
||||
|
||||
def _inline(text: str) -> str:
|
||||
text = _esc(text)
|
||||
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1", text)
|
||||
text = re.sub(r"`([^`]+)`", r"<font face='Courier' size='8'>\1</font>", text)
|
||||
text = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", text)
|
||||
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", text)
|
||||
return text.replace("|", "/")
|
||||
|
||||
|
||||
def _ascii_code(text: str) -> str:
|
||||
repl = {
|
||||
"│": "|", "─": "-", "┌": "+", "┐": "+", "└": "+", "┘": "+",
|
||||
"├": "+", "┤": "+", "┬": "+", "┴": "+", "┼": "+",
|
||||
"►": ">", "▼": "v", "▲": "^", "◄": "<",
|
||||
"→": "->", "←": "<-", "↔": "<->", "⇒": "=>",
|
||||
"—": "--", "–": "-", "…": "...", "×": "x",
|
||||
}
|
||||
for a, b in repl.items():
|
||||
text = text.replace(a, b)
|
||||
return text
|
||||
|
||||
|
||||
def _table(header: list[str], rows: list[list[str]]):
|
||||
n = max(1, len(header))
|
||||
# weight first column a bit if many cols
|
||||
if n == 1:
|
||||
widths = [USABLE]
|
||||
elif n == 2:
|
||||
widths = [2.2 * inch, USABLE - 2.2 * inch]
|
||||
else:
|
||||
first = 1.5 * inch
|
||||
rest = (USABLE - first) / (n - 1)
|
||||
widths = [first] + [rest] * (n - 1)
|
||||
if n >= 4:
|
||||
widths = [USABLE / n] * n
|
||||
data = [[Paragraph(_inline(h), S["cell_h"]) for h in header]]
|
||||
for row in rows:
|
||||
padded = (row + [""] * n)[:n]
|
||||
data.append([Paragraph(_inline(c), S["cell"]) for c in padded])
|
||||
t = Table(data, colWidths=widths, repeatRows=1)
|
||||
cmds = [
|
||||
("BACKGROUND", (0, 0), (-1, 0), NAVY),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 3),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 3),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 3),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
||||
("GRID", (0, 0), (-1, -1), 0.3, RULE),
|
||||
("LINEBELOW", (0, 0), (-1, 0), 1, TEAL),
|
||||
]
|
||||
for i in range(1, len(data)):
|
||||
if i % 2 == 0:
|
||||
cmds.append(("BACKGROUND", (0, i), (-1, i), ROW))
|
||||
t.setStyle(TableStyle(cmds))
|
||||
return t
|
||||
|
||||
|
||||
def _split_row(line: str) -> list[str]:
|
||||
line = line.strip().strip("|")
|
||||
return [c.strip() for c in line.split("|")]
|
||||
|
||||
|
||||
def md_to_flowables(md: str, skip_first_h1: bool = False) -> list:
|
||||
lines = md.replace("\r\n", "\n").split("\n")
|
||||
out = []
|
||||
i = 0
|
||||
skipped = False
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
if line.startswith("```"):
|
||||
buf = []
|
||||
i += 1
|
||||
while i < len(lines) and not lines[i].startswith("```"):
|
||||
buf.append(lines[i])
|
||||
i += 1
|
||||
i += 1
|
||||
block = _ascii_code("\n".join(buf))
|
||||
out.append(Preformatted(block + "\n", S["code"]))
|
||||
continue
|
||||
|
||||
if re.match(r"^\s*\|", line) and i + 1 < len(lines) and re.search(r"\|\s*-+", lines[i + 1]):
|
||||
header = _split_row(line)
|
||||
i += 2
|
||||
rows = []
|
||||
while i < len(lines) and re.match(r"^\s*\|", lines[i]):
|
||||
rows.append(_split_row(lines[i]))
|
||||
i += 1
|
||||
out.append(_table(header, rows))
|
||||
out.append(Spacer(1, 6))
|
||||
continue
|
||||
|
||||
if line.strip() == "---":
|
||||
out.append(Spacer(1, 8))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if line.startswith("# "):
|
||||
if skip_first_h1 and not skipped:
|
||||
skipped = True
|
||||
i += 1
|
||||
continue
|
||||
out.append(Paragraph(_inline(line[2:].strip()), S["h1"]))
|
||||
i += 1
|
||||
continue
|
||||
if line.startswith("## "):
|
||||
out.append(Paragraph(_inline(line[3:].strip()), S["h2"]))
|
||||
i += 1
|
||||
continue
|
||||
if line.startswith("### "):
|
||||
out.append(Paragraph(_inline(line[4:].strip()), S["h3"]))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
m = re.match(r"^(\s*)[-*]\s+(.*)$", line)
|
||||
if m:
|
||||
out.append(Paragraph("• " + _inline(m.group(2)), S["bullet"]))
|
||||
i += 1
|
||||
continue
|
||||
m = re.match(r"^(\s*)\d+\.\s+(.*)$", line)
|
||||
if m:
|
||||
out.append(Paragraph("• " + _inline(m.group(2)), S["bullet"]))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if not line.strip():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
out.append(Paragraph(_inline(line.strip()), S["body"]))
|
||||
i += 1
|
||||
return out
|
||||
|
||||
|
||||
def header_footer_factory(kicker: str):
|
||||
def header_footer(canvas, doc):
|
||||
canvas.saveState()
|
||||
w, h = letter
|
||||
canvas.setFillColor(NAVY)
|
||||
canvas.rect(0, h - 28, w, 28, fill=1, stroke=0)
|
||||
canvas.setFillColor(colors.white)
|
||||
canvas.setFont("Helvetica", 8)
|
||||
canvas.drawString(0.7 * inch, h - 18, "Verae Time x Zapier")
|
||||
canvas.drawRightString(w - 0.7 * inch, h - 18, kicker[:70])
|
||||
canvas.setFillColor(TEAL)
|
||||
canvas.rect(0, 0, w, 22, fill=1, stroke=0)
|
||||
canvas.setFillColor(colors.white)
|
||||
canvas.setFont("Helvetica", 8)
|
||||
canvas.drawString(0.7 * inch, 8, "Internal · 18 August 2026")
|
||||
canvas.drawRightString(w - 0.7 * inch, 8, str(doc.page))
|
||||
canvas.restoreState()
|
||||
|
||||
return header_footer
|
||||
|
||||
|
||||
def cover_footer(canvas, doc):
|
||||
header_footer_factory("")(canvas, doc)
|
||||
|
||||
|
||||
def build_pdf(parts: list[tuple[str, str]], dest: Path, title: str, subtitle: str):
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
story = [
|
||||
Spacer(1, 1.7 * inch),
|
||||
Paragraph("VERAE TIME · ZAPIER WORKSPACE", S["cover_kicker"]),
|
||||
Paragraph(_esc(title).replace("\n", "<br/>"), S["cover_title"]),
|
||||
Paragraph(_esc(subtitle), S["caption"]),
|
||||
PageBreak(),
|
||||
]
|
||||
for idx, (label, md) in enumerate(parts):
|
||||
if idx:
|
||||
story.append(PageBreak())
|
||||
if label:
|
||||
story.append(Paragraph(_inline(label), S["h1"]))
|
||||
story.extend(md_to_flowables(md, skip_first_h1=bool(label)))
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
str(dest),
|
||||
pagesize=letter,
|
||||
leftMargin=0.7 * inch,
|
||||
rightMargin=0.7 * inch,
|
||||
topMargin=0.55 * inch,
|
||||
bottomMargin=0.45 * inch,
|
||||
title=title,
|
||||
author="Verae / Zapier research workspace",
|
||||
subject=subtitle,
|
||||
)
|
||||
hf = header_footer_factory(title)
|
||||
doc.build(story, onFirstPage=cover_footer, onLaterPages=hf)
|
||||
print(f"wrote {dest} ({dest.stat().st_size} bytes)")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) >= 2 and argv[0] == "--concat":
|
||||
dest = Path(argv[1])
|
||||
title = argv[2]
|
||||
files = [Path(p) for p in argv[3:]]
|
||||
parts = [(p.name, p.read_text(encoding="utf-8")) for p in files]
|
||||
build_pdf(parts, dest, title, "Combined from " + ", ".join(p.name for p in files))
|
||||
return 0
|
||||
if len(argv) < 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
src = Path(argv[0])
|
||||
dest = Path(argv[1])
|
||||
title = argv[2] if len(argv) > 2 else src.stem.replace("-", " ").title()
|
||||
build_pdf([(None, src.read_text(encoding="utf-8"))], dest, title, str(src))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Loading…
Add table
Add a link
Reference in a new issue