112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
#!/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()
|