feat: rename to paperspec and add the website
- Rename manuspec to paperspec everywhere outside pixi.toml: prose, the tools, provenance tool names and the fixture key derivations, so every fixture and vector is rebuilt. - Move the namespace addresses under the site, mirroring the repository: schema $id https://paperspec.scient.ing/schemas/<file>, the JSON-LD context https://paperspec.scient.ing/context/scienting-1.jsonld, and term IRIs https://paperspec.scient.ing/vocab#<term>. Format identifiers (scienting-*) and URNs (urn:scienting:) are unchanged. - tools/site.py (standard library only) builds _site/: every file of the repository at its own path, each Markdown file rendered as a page with sidebar navigation, heading anchors matching the forge's, folder listings, a vocabulary page for the context's IRIs, a heading search and a 404 page; light and dark themes. - check.py builds the site and checks that every Markdown file has a page, every internal link and fragment resolves, no Markdown is left unrendered, and every $id, the context address and the vocabulary IRIs are files of the site. - deploy/: a Caddyfile with the media types and CORS the data needs, and a guide; .gitea/workflows/site.yml builds on main and publishes with rsync when the SITE_* secrets are set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
213 files changed
+1233
-386
No files matched your search
+85
-2
@@ -37,7 +37,7 @@ from tools.oracle.tree import Refusal # noqa: E402
|
||||
|
||||
FAILS = []
|
||||
# Folders that are not part of the repository: version control, environments and caches.
|
||||
SKIP_DIRS = {".git", ".pixi", "node_modules", "__pycache__"}
|
||||
SKIP_DIRS = {".git", ".pixi", "node_modules", "__pycache__", "_site", ".claude"}
|
||||
COUNTS = {}
|
||||
|
||||
|
||||
@@ -852,7 +852,7 @@ COUNT_PATTERNS = [
|
||||
(NUM + r" cases in all\b", "cases"),
|
||||
]
|
||||
# Where the prose names the specification's own version; other standards' versions are not checked.
|
||||
VERSION_PATTERN = r"\b(?:manuspec|the formats are|the specification is at|this is|it is|is at|at) version (\d+\.\d+\.\d+)\b|\bmanuspec (\d+\.\d+\.\d+)\b"
|
||||
VERSION_PATTERN = r"\b(?:paperspec|the formats are|the specification is at|this is|it is|is at|at) version (\d+\.\d+\.\d+)\b|\bpaperspec (\d+\.\d+\.\d+)\b"
|
||||
|
||||
|
||||
def check_counts():
|
||||
@@ -907,6 +907,88 @@ def check_counts():
|
||||
% (rel, i, word, what, have[what]))
|
||||
|
||||
|
||||
def check_site():
|
||||
"""The website builds, every Markdown file has a page, every internal link and fragment resolves, no Markdown
|
||||
is left unrendered, and the namespace addresses the formats name are files the site serves."""
|
||||
from html.parser import HTMLParser
|
||||
from tools import site
|
||||
|
||||
class Page(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.ids, self.links, self.text, self.skip = set(), [], [], 0
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
a = dict(attrs)
|
||||
if "id" in a:
|
||||
self.ids.add(a["id"])
|
||||
for k in ("href", "src"):
|
||||
if a.get(k):
|
||||
self.links.append(a[k])
|
||||
if tag in ("code", "pre", "script", "style"):
|
||||
self.skip += 1
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag in ("code", "pre", "script", "style"):
|
||||
self.skip -= 1
|
||||
|
||||
def handle_data(self, data):
|
||||
if not self.skip:
|
||||
self.text.append(data)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = os.path.join(tmp, "site")
|
||||
site.build(out)
|
||||
pages = {}
|
||||
for d, _, fs in os.walk(out):
|
||||
for f in fs:
|
||||
if f.endswith(".html"):
|
||||
pg = Page()
|
||||
with open(os.path.join(d, f), encoding="utf-8") as fh:
|
||||
pg.feed(fh.read())
|
||||
pages[os.path.relpath(os.path.join(d, f), out)] = pg
|
||||
for d, dirs, fs in os.walk(ROOT):
|
||||
dirs[:] = [x for x in dirs if x not in SKIP_DIRS]
|
||||
for f in fs:
|
||||
if f.endswith(".md"):
|
||||
md = os.path.relpath(os.path.join(d, f), ROOT)
|
||||
ok("site", site.page_path(md) in pages, "%s has no page" % md)
|
||||
for name, pg in sorted(pages.items()):
|
||||
if name == "404.html":
|
||||
continue
|
||||
# Markdown syntax left in a single text node means a construct the renderer did not recognize.
|
||||
bad = [m.group(0) for chunk in pg.text
|
||||
for m in re.finditer(r"\]\(|\*\*|`|\n *(?:\||: |#{1,6} |[-*] |\d+\. )", chunk)]
|
||||
ok("site", not bad, "%s: unrendered Markdown %r" % (name, sorted(set(bad))[:3]))
|
||||
for link in pg.links:
|
||||
if re.match(r"^[a-z][a-z0-9+.-]*:", link) or link.startswith("//"):
|
||||
continue
|
||||
path, _, frag = link.partition("#")
|
||||
target = os.path.normpath(os.path.join(os.path.dirname(name), path)) if path else name
|
||||
full = os.path.join(out, target)
|
||||
if os.path.isdir(full):
|
||||
target = os.path.join(target, "index.html")
|
||||
full = os.path.join(out, target)
|
||||
ok("site", os.path.exists(full), "%s: link to a missing file %s" % (name, link))
|
||||
if frag and target in pages:
|
||||
ok("site", frag in pages[target].ids, "%s: no #%s in %s" % (name, frag, target))
|
||||
# The addresses the formats name are files of the site.
|
||||
for f in SCHEMA_FILES:
|
||||
sid = STORE.load(f)["$id"]
|
||||
ok("site", sid.startswith(site.BASE_URL + "/") and
|
||||
os.path.exists(os.path.join(out, sid[len(site.BASE_URL) + 1:])), "%s is not served at its $id" % f)
|
||||
ctx_url = jl(os.path.join(p("fixtures", "records", "layer"), "annotations.jsonl"))[0]["@context"][1]
|
||||
ok("site", ctx_url.startswith(site.BASE_URL + "/") and
|
||||
os.path.exists(os.path.join(out, ctx_url[len(site.BASE_URL) + 1:])), "the context is not served at %s" % ctx_url)
|
||||
vocab = js(p("context", "scienting-1.jsonld"))["@context"]["sct"]
|
||||
ok("site", vocab == site.BASE_URL + "/vocab#", "the vocabulary IRI %s is not the site's vocab page" % vocab)
|
||||
terms = [v if isinstance(v, str) else v["@id"] for k, v in js(p("context", "scienting-1.jsonld"))["@context"].items()
|
||||
if not k.startswith("@")]
|
||||
for t in terms:
|
||||
if t.startswith("sct:"):
|
||||
ok("site", t[4:] in pages["vocab/index.html"].ids, "the vocabulary page has no #%s" % t[4:])
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(
|
||||
@@ -932,6 +1014,7 @@ def main():
|
||||
check_counts()
|
||||
check_prose()
|
||||
check_links()
|
||||
check_site()
|
||||
for k in sorted(COUNTS):
|
||||
print("%-17s %5d checks" % (k, COUNTS[k]))
|
||||
if FAILS:
|
||||
|
||||
Reference in new issue
Block a user