lindisfarner 0.1.2

Illuminate or vandalize text and code with ASCII art in Rust.
Documentation
#!/usr/bin/env python3
"""Regenerate the README banner (assets/banner.svg).

Renders the most elaborate page lindisfarner can produce — every manuscript
element at once — and converts its ANSI output into a self-contained SVG that
mirrors a real terminal: the gold illuminated initial, a rubricated incipit and
words, the ornate ❦ frame, the drollery menagerie down the margin, a justified
two-column codex, alternating red/blue ¶ pilcrows, and ❧ line fillers.
Characters are placed on an exact grid so alignment does not depend on the
viewer's font.

Usage:  python3 scripts/render-banner.py
"""

import html
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "assets" / "banner.svg"

# Theme: a dark terminal with warm "gold leaf" pigments.
BG, CARD = "#171717", "#3a3320"
INK = "#ddd6c6"  # body ink
GOLD = "#d4b24a"  # frame / border  (ANSI 33)
GOLDLF = "#f4cb4b"  # illuminated initial  (ANSI 1;33)
RED = "#e5675a"  # rubric / odd pilcrow  (ANSI 31)
BLUE = "#6a93d4"  # alternating pilcrow  (ANSI 34)

FS, CW, LH, PAD = 15.5, 9.3, 18.5, 18.0
FONT = "ui-monospace, SFMono-Regular, Menlo, Consolas, "DejaVu Sans Mono", monospace"


def ansi() -> str:
    """The colourful default rendering of the sample."""
    exe = ROOT / "target" / "release" / "lindisfarner"
    cmd = [str(exe)] if exe.exists() else ["cargo", "run", "-q", "--release", "--"]
    # The most elaborate page: every manuscript element at once — illuminated
    # drop cap, rubricated incipit and words, gold ornate border, the drollery
    # menagerie, a justified two-column codex, alternating pilcrows, and fillers.
    out = subprocess.run(
        cmd
        + [
            "sample.txt",
            "-c",
            "always",
            "--columns",
            "2",
            "--justify",
            "--incipit",
            "--fillers",
            "--drolleries",
            "--pilcrows",
            "-r",
            "kidneys,bloom,cat,tea",
            "-w",
            "96",
        ],
        cwd=ROOT,
        capture_output=True,
        text=True,
        check=True,
    )
    return out.stdout


def style(codes, bold):
    if 33 in codes:
        return (GOLDLF if bold else GOLD, bold)
    if 31 in codes:
        return (RED, bold)  # rubricated words and odd pilcrows
    if 34 in codes:
        return (BLUE, bold)  # the alternating blue pilcrow
    return (INK, bold)


def runs(line):
    out, cur, codes, bold, i = [], "", set(), False, 0
    while i < len(line):
        c = line[i]
        if c == "\x1b" and line[i + 1 : i + 2] == "[":
            j = line.index("m", i)
            if cur:
                out.append((cur, *style(codes, bold)))
                cur = ""
            for n in [int(x) for x in line[i + 2 : j].split(";") if x] or [0]:
                if n == 0:
                    codes.clear()
                    bold = False
                elif n == 1:
                    bold = True
                else:
                    codes.add(n)
            i = j + 1
        else:
            cur += c
            i += 1
    if cur:
        out.append((cur, *style(codes, bold)))
    return out


def main():
    lines = ansi().split("\n")
    while lines and lines[-1] == "":
        lines.pop()
    cols = max(sum(len(t) for t, _, _ in runs(l)) for l in lines)
    w, h = PAD * 2 + cols * CW, PAD * 2 + len(lines) * LH

    s = [
        "<!-- Generated by scripts/render-banner.py — the most elaborate lindisfarner page. Do not edit by hand. -->",
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{w:.0f}" height="{h:.0f}" '
        f'viewBox="0 0 {w:.0f} {h:.0f}" font-family="{FONT}" font-size="{FS}">',
        f'<rect x="0.5" y="0.5" width="{w - 1:.0f}" height="{h - 1:.0f}" rx="11" fill="{BG}" stroke="{CARD}"/>',
    ]
    for row, line in enumerate(lines):
        y = PAD + row * LH + FS * 0.78
        col = 0
        for text, color, bold in runs(line):
            seg, segx = "", None

            def flush(buf, bx):
                weight = ' font-weight="bold"' if bold else ""
                xs = " ".join(f"{PAD + (bx + k) * CW:.2f}" for k in range(len(buf)))
                s.append(
                    f'<text xml:space="preserve" y="{y:.2f}" x="{xs}" '
                    f'fill="{color}"{weight}>{html.escape(buf)}</text>'
                )

            for ch in text:
                if ch == " ":
                    if seg:
                        flush(seg, segx)
                        seg, segx = "", None
                    col += 1
                else:
                    if not seg:
                        segx = col
                    seg += ch
                    col += 1
            if seg:
                flush(seg, segx)
    s.append("</svg>")
    OUT.write_text("\n".join(s) + "\n")
    print(f"wrote {OUT.relative_to(ROOT)} ({w:.0f}x{h:.0f}, {cols} cols, {len(lines)} rows)")


if __name__ == "__main__":
    sys.exit(main())