Files
matrix/landing/build.py
T

112 lines
4.4 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Render the data-driven sections of landing/index.html.
The fork feature list and the client comparison table used to be edited by
hand in two places per feature (matrix #11). They now live in landing/data/:
features.json - the "our fork adds" groups, one short line per feature
comparison.json - the client comparison table + its "reviewed" date
and the stylesheet lives in landing/style.css. It is inlined into index.html
rather than linked, because the LXC 139 deploy copies index.html only.
Run `python3 landing/build.py` after editing either file and commit the
regenerated index.html (LXC 139 serves the files as-is; there is no build step
there). `--check` exits 1 if index.html is out of date, for CI.
Only the text between `<!-- BEGIN:name -->` and `<!-- END:name -->` markers is
rewritten; everything else in index.html stays hand-edited.
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
INDEX = ROOT / "index.html"
MARK = {"yes": "✓", "no": "✗", "part": "~"}
def render_features(groups):
out = ['<div class="fork-features">']
for g in groups:
out.append(' <div class="fork-group">')
out.append(f' <h4>{g["title"]}</h4>')
out.append(" <ul>")
out.extend(f" <li>{item}</li>" for item in g["items"])
out.append(" </ul>")
out.append(" </div>")
out.append("</div>")
return "\n".join(out)
def render_cell(cell, ours):
cls = ' class="ours"' if ours else ""
if "mark" in cell:
inner = f'<span class="{cell["mark"]}">{MARK[cell["mark"]]}</span>'
else:
inner = cell["text"]
if cell.get("note"):
inner += f'<small>{cell["note"]}</small>'
return f"<td{cls}>{inner}</td>"
def render_comparison(data):
clients = data["clients"]
cols = len(clients) + 1
out = ["<table>", " <thead>", " <tr>", " <th></th>"]
for i, c in enumerate(clients):
cls = ' class="ours"' if i == 0 else ""
sub = f'<small>{c["sub"]}</small>' if c["sub"] else ""
out.append(f' <th{cls}>{c["name"]}{sub}</th>')
out += [" </tr>", " </thead>", " <tbody>"]
for sec in data["sections"]:
out.append(f' <tr class="section-header"><td colspan="{cols}">{sec["title"]}</td></tr>')
for row in sec["rows"]:
if len(row["cells"]) != len(clients):
sys.exit(f'comparison.json: "{row["feature"]}" has {len(row["cells"])} cells, expected {len(clients)}')
out.append(" <tr>")
out.append(f' <td>{row["feature"]}</td>')
out.extend(f" {render_cell(c, i == 0)}" for i, c in enumerate(row["cells"]))
out.append(" </tr>")
out += [" </tbody>", "</table>"]
return "\n".join(out)
def replace_block(html, name, content):
pattern = re.compile(
rf"(?P<indent>[ \t]*)<!-- BEGIN:{name} -->.*?<!-- END:{name} -->", re.S
)
m = pattern.search(html)
if not m:
sys.exit(f"index.html: missing <!-- BEGIN:{name} --> / <!-- END:{name} --> markers")
indent = m.group("indent")
body = "\n".join(indent + line if line else line for line in content.split("\n"))
block = f"{indent}<!-- BEGIN:{name} -->\n{body}\n{indent}<!-- END:{name} -->"
return html[: m.start()] + block + html[m.end() :]
def main():
features = json.loads((ROOT / "data" / "features.json").read_text(encoding="utf-8"))
comparison = json.loads((ROOT / "data" / "comparison.json").read_text(encoding="utf-8"))
css = (ROOT / "style.css").read_text(encoding="utf-8").rstrip("\n")
html = INDEX.read_text(encoding="utf-8")
styles = "<style>\n" + "\n".join(" " + line if line else line for line in css.split("\n")) + "\n</style>"
new = replace_block(html, "styles", styles)
new = replace_block(new, "features", render_features(features))
new = replace_block(new, "comparison", render_comparison(comparison))
new = replace_block(
new, "reviewed", f'Client comparison last reviewed <time datetime="{comparison["reviewed"]}">{comparison["reviewed"]}</time>'
)
if "--check" in sys.argv:
if new != html:
sys.exit("landing/index.html is out of date: run python3 landing/build.py")
print("landing/index.html is up to date")
return
INDEX.write_text(new, encoding="utf-8")
print("wrote", INDEX)
if __name__ == "__main__":
main()