"""Builds the CSSWG directory index.
It also sets up redirects from shortnames to the current work spec, by building
an index.html with a .
"""
import json
import os
import os.path
import re
import sys
import subprocess
from collections import defaultdict
from html.parser import HTMLParser
from bikeshed import Spec, constants
import jinja2
jinja_env = jinja2.Environment(
loader=jinja2.PackageLoader("build-index", "templates"),
autoescape=jinja2.select_autoescape(),
trim_blocks=True,
lstrip_blocks=True
)
def title_from_html(file):
class HTMLTitleParser(HTMLParser):
def __init__(self):
super().__init__()
self.in_title = False
self.title = ""
self.done = False
def handle_starttag(self, tag, attrs):
if tag == "title":
self.in_title = True
def handle_data(self, data):
if self.in_title:
self.title += data
def handle_endtag(self, tag):
if tag == "title" and self.in_title:
self.in_title = False
self.done = True
self.reset()
parser = HTMLTitleParser()
with open(file, encoding="UTF-8") as f:
for line in f:
parser.feed(line)
if parser.done:
break
if not parser.done:
parser.close()
return parser.title if parser.done else None
def get_date_authored_timestamp_from_git(path):
source = os.path.realpath(path)
proc = subprocess.run(["git", "log", "-1", "--format=%at", source],
capture_output = True, encoding = "utf_8")
return int(proc.stdout.splitlines()[-1])
def get_bs_spec_metadata(folder_name, path):
spec = Spec(path)
spec.assembleDocument()
level = int(spec.md.level) if spec.md.level else 0
if spec.md.shortname == "css-animations-2":
shortname = "css-animations"
elif spec.md.shortname == "css-gcpm-4":
shortname = "css-gcpm"
elif spec.md.shortname == "css-transitions-2":
shortname = "css-transitions"
elif spec.md.shortname == "scroll-animations-1":
shortname = "scroll-animations"
else:
# Fix CSS snapshots (e.g. "css-2022")
snapshot_match = re.match(
"^css-(20[0-9]{2})$", spec.md.shortname)
if snapshot_match:
shortname = "css-snapshot"
level = int(snapshot_match.group(1))
else:
shortname = spec.md.shortname
return {
"timestamp": get_date_authored_timestamp_from_git(path),
"shortname": shortname,
"level": level,
"title": spec.md.title,
"workStatus": spec.md.workStatus
}
def get_html_spec_metadata(folder_name, path):
match = re.match("^([a-z0-9-]+)-([0-9]+)$", folder_name)
if match and match.group(1) == "css":
shortname = "css-snapshot"
title = f"CSS Snapshot {match.group(2)}"
else:
shortname = match.group(1) if match else folder_name
title = title_from_html(path)
return {
"shortname": shortname,
"level": int(match.group(2)) if match else 0,
"title": title,
"workStatus": "completed" # It's a good heuristic
}
def create_symlink(shortname, spec_folder):
"""Creates a symlink pointing to the given .
"""
if spec_folder in timestamps:
timestamps[shortname] = timestamps[spec_folder]
try:
os.symlink(spec_folder, shortname)
except OSError:
pass
CURRENT_WORK_EXCEPTIONS = {
"css-conditional": 5,
"css-easing": 2,
"css-grid": 2,
"css-snapshot": None, # always choose the last spec
"css-values": 4,
"css-writing-modes": 4,
"web-animations": 2
}
# ------------------------------------------------------------------------------
constants.setErrorLevel("nothing")
specgroups = defaultdict(list)
timestamps = defaultdict(list)
for entry in os.scandir("."):
if entry.is_dir(follow_symlinks=False):
# Not actual specs, just examples.
if entry.name in ["css-module"]:
continue
bs_file = os.path.join(entry.path, "Overview.bs")
html_file = os.path.join(entry.path, "Overview.html")
if os.path.exists(bs_file):
metadata = get_bs_spec_metadata(entry.name, bs_file)
timestamps[entry.name] = metadata["timestamp"]
elif os.path.exists(html_file):
metadata = get_html_spec_metadata(entry.name, html_file)
else:
# Not a spec
continue
metadata["dir"] = entry.name
metadata["currentWork"] = False
specgroups[metadata["shortname"]].append(metadata)
# Reorder the specs with common shortname based on their level (or year, for
# CSS snapshots), and determine which spec is the current work.
for shortname, specgroup in specgroups.items():
if len(specgroup) == 1:
if shortname != specgroup[0]["dir"]:
create_symlink(shortname, specgroup[0]["dir"])
else:
specgroup.sort(key=lambda spec: spec["level"])
# TODO: This algorithm for determining which spec is the current work
# is wrong in a number of cases. Try and come up with a better
# algorithm, rather than maintaining a list of exceptions.
for spec in specgroup:
if shortname in CURRENT_WORK_EXCEPTIONS:
if CURRENT_WORK_EXCEPTIONS[shortname] == spec["level"]:
spec["currentWork"] = True
currentWorkDir = spec["dir"]
break
elif spec["workStatus"] != "completed":
spec["currentWork"] = True
currentWorkDir = spec["dir"]
break
else:
specgroup[-1]["currentWork"] = True
currentWorkDir = specgroup[-1]["dir"]
if shortname != currentWorkDir:
create_symlink(shortname, currentWorkDir)
if shortname == "css-snapshot":
create_symlink("css", currentWorkDir)
with open('./timestamps.json', 'w') as f:
json.dump(dict(sorted(timestamps.items())), f, indent = 2)
with open("./index.html", mode='w', encoding="UTF-8") as f:
template = jinja_env.get_template("index.html.j2")
f.write(template.render(specgroups=specgroups))