#!/usr/bin/env python3 """Build the two-page Tiny Tapeout product datasheet. All copy, numbers and colours live in datasheet.yaml. This script renders them through datasheet.html.mustache, inlining the font and photo as data URIs so the result is a single self-contained file that prints to two A4 pages. python3 docs/product/build.py # write tt-datasheet.html python3 docs/product/build.py --pdf # also render tt-datasheet.pdf The sheets are a fixed 297mm tall with overflow hidden, so copy that grows too long is silently clipped rather than spilling onto a third page. Every build therefore measures the rendered sheets in headless Chrome and fails if anything overflows. Pass --no-check to skip that (or set CHROME if it isn't found). Requires chevron and pyyaml, both already pinned in requirements.txt. """ import argparse import base64 import logging import os import re import shutil import subprocess import sys from pathlib import Path import chevron import yaml HERE = Path(__file__).parent # `style` tokens in datasheet.yaml -> CSS classes in the template. STYLE_CLASSES = { "name": "t-name", "mono": "t-mono", "num": "t-num", "price": "t-price", "dim": "t-dim", "center": "t-center", } # Tables are rendered through partials/table.mustache and need preparing first. TABLE_KEYS = ["pricing", "process", "ratings", "gpio", "analog", "ordering"] EM_DASH = "—" CHROME_CANDIDATES = [ os.environ.get("CHROME"), "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "google-chrome", "chromium", ] def cell_classes(style: str, value: str) -> str: """Map a column's style tokens to CSS classes for one cell.""" classes = [] for token in style.split(): try: classes.append(STYLE_CLASSES[token]) except KeyError: raise SystemExit( f"unknown style '{token}' — expected one of {', '.join(STYLE_CLASSES)}" ) # A lone em dash means "not applicable"; grey it out wherever it appears. if value.strip() == EM_DASH and "t-dim" not in classes: classes.append("t-dim") return " ".join(classes) def prepare_table(name: str, table: dict) -> None: """Expand a table's columns and rows into what the template expects.""" columns = table.get("columns") or [] for column in columns: width = column.get("width") column["width_attr"] = f' style="width:{width}"' if width else "" column.setdefault("label", "") styles = [column.get("style", "") for column in columns] rows = [] for index, row in enumerate(table.get("rows") or [], start=1): if len(row) != len(columns): raise SystemExit( f"{name}: row {index} has {len(row)} cells " f"but there are {len(columns)} columns" ) rows.append( { "cells": [ {"v": value, "cls": cell_classes(style, value)} for style, value in zip(styles, row) ] } ) table["rows"] = rows table["has_note"] = bool(table.get("note")) table["has_footnote"] = bool(table.get("footnote")) def as_data_uri_payload(path: Path) -> str: if not path.is_file(): raise SystemExit(f"missing asset: {path}") return base64.b64encode(path.read_bytes()).decode() def find_chrome() -> str: for candidate in CHROME_CANDIDATES: if candidate and (Path(candidate).is_file() or shutil.which(candidate)): return candidate raise SystemExit( "could not find Chrome — set the CHROME environment variable to its path" ) MEASURE_SCRIPT = """