Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions respec/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@
const respecConfig = {
// Working Groups ids at https://respec.org/w3c/groups/
// group: "Semantic Data Products Working Group",
latestVersion: "https://www.omg.org/spec/DPROD/dprod/",
edDraftURI: "https://ekgf.org/spec/{{ branch }}/",
// The OMG catalog entry, which always resolves to the newest published
// version. Not the ontology namespace IRI (.../DPROD/dprod/): that
// identifies the vocabulary, not a document, and 404s (issue #255).
latestVersion: "https://www.omg.org/spec/DPROD/",
// The one canonical continuously-updated draft, whichever branch this
// build came from. A branch preview points readers at mainline; its own
// URL is thisVersion below.
edDraftURI: "https://ekgf.org/dprod/spec/develop/",
// This build. The slug is the branch with slashes replaced by dashes,
// and the /dprod basePath is required — without it the URL 404s.
thisVersion: "https://ekgf.org/dprod/spec/{{ branch_slug }}/",
specStatus: "base",
// Pinned to the branch's last commit by the spec generator. Without it
// ReSpec uses the date the page is viewed, since it runs in the reader's
Expand Down Expand Up @@ -148,7 +157,10 @@
},
dprod: {
title: "Data Product Ontology",
href: "https://www.omg.org/spec/DPROD/dprod/",
// The OMG catalog entry, which resolves. The vocabulary namespace
// IRI (.../DPROD/dprod/) identifies the terms rather than a
// document, and does not currently dereference (issue #255).
href: "https://www.omg.org/spec/DPROD/",
authors: [
"Tony Seale",
"Natasa Varytimou",
Expand Down Expand Up @@ -393,7 +405,7 @@
</p>
<p>
For a complete list of trademarks,
see: <a href="https://www.omg.org/legal/tm_list.htm.">https://www.omg.org/legal/tm_list.htm</a>.
see: <a href="https://www.omg.org/legal/tm_list.htm">https://www.omg.org/legal/tm_list.htm</a>.
All other products or company names mentioned are used for identification purposes only and may be
trademarks of their respective owners.
</p>
Expand Down Expand Up @@ -457,7 +469,7 @@ <h1 id="title">Data Product Ontology (DPROD)</h1>
<section>
<h2>Scope</h2>
<p>
The <a href="https://www.omg.org/spec/DPROD/dprod.ttl">Data Product (DPROD)</a> specification is a
The <a href="dprod.ttl">Data Product (DPROD)</a> specification is a
profile of the <a href="https://www.w3.org/TR/vocab-dcat-3/">Data Catalog (DCAT) Vocabulary</a>,
designed to describe Data Products.
This document defines the schema and provides examples of its use.
Expand All @@ -470,7 +482,7 @@ <h2>Scope</h2>
and enables federated search across multiple sites using a uniform query mechanism and structure.
</p>
<p>
The namespace for DPROD terms is <a href="https://www.omg.org/spec/DPROD/dprod/">https://www.omg.org/spec/DPROD/dprod/</a>
The namespace for DPROD terms is <code>https://www.omg.org/spec/DPROD/dprod/</code>
</p>
<p>
The suggested prefix for the DPROD namespace is <strong><code>dprod</code></strong>
Expand All @@ -484,7 +496,7 @@ <h2>Scope</h2>
</li>
<li>
Harmonize Data Schemas: Using shared schemas helps unify different data formats.
For instance, the <a href="https://www.omg.org/spec/DPROD/dprod.ttl">DPROD</a> specification provides a
For instance, the <a href="dprod.ttl">DPROD</a> specification provides a
<!--suppress HtmlUnknownAnchorTarget -->
common set of rules for defining a <a href="#dataproduct">Data Product</a>.
Users of DPROD can extend this schema as needed.
Expand Down
12 changes: 10 additions & 2 deletions site/public/spec/archive/1.0/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
const respecConfig = {
// Working Groups ids at https://respec.org/w3c/groups/
// group: "Semantic Data Products Working Group",
latestVersion: "https://ekgf.org/dprod/spec/main/",
// The OMG catalog entry, so a reader of this frozen 1.0 can reach the
// newest published version. It previously pointed at this document
// itself, which is what "this version" means, not "latest".
latestVersion: "https://www.omg.org/spec/DPROD/",
// Set explicitly. When edDraftURI is absent ReSpec infers it from the
// github config as https://ekgf.github.io/dprod/ — GitHub Pages, retired
// by #235, so every reader of the frozen standard met a 404 (issue #255).
edDraftURI: "https://ekgf.org/dprod/spec/develop/",
thisVersion: "https://ekgf.org/dprod/spec/main/",
specStatus: "base",
// Frozen: DPROD 1.0 beta 1. Hard-coded rather than derived, because this
// document is immutable and its date must not move when a maintenance
Expand Down Expand Up @@ -389,7 +397,7 @@
</p>
<p>
For a complete list of trademarks,
see: <a href="https://www.omg.org/legal/tm_list.htm.">https://www.omg.org/legal/tm_list.htm</a>.
see: <a href="https://www.omg.org/legal/tm_list.htm">https://www.omg.org/legal/tm_list.htm</a>.
All other products or company names mentioned are used for identification purposes only and may be
trademarks of their respective owners.
</p>
Expand Down
11 changes: 11 additions & 0 deletions spec-generator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ def detect_branch() -> str:
return "main"


def branch_slug(branch: str) -> str:
"""The branch name as it appears in a /spec/<slug>/ URL.

Slashes become dashes, matching site/src/lib/spec-versions.ts. The raw
branch name is still what ReSpec's `github` config needs for its commit
history link, so both are passed to the template.
"""
return branch.replace("/", "-")


def detect_publish_date(g) -> str:
"""The publication date of this version, from `dct:issued` on the ontology.

Expand Down Expand Up @@ -127,6 +137,7 @@ def main():
'classes': classes,
'examples': examples,
'branch': detect_branch(),
'branch_slug': branch_slug(detect_branch()),
'publish_date': detect_publish_date(g),
})

Expand Down
107 changes: 107 additions & 0 deletions tests/test_spec_header_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""The spec header must not advertise URLs that 404.

Three separate dead links shipped at once (issue #255):

* `edDraftURI` pointed at `https://ekgf.org/spec/{branch}/` — missing the
`/dprod` basePath, and with the branch name unslugified, so a branch
containing a slash could never resolve;
* `latestVersion` pointed at the *vocabulary namespace* IRI, which identifies
the terms rather than a document and does not dereference;
* the frozen 1.0 archive set no `edDraftURI` at all, so ReSpec inferred one
from the `github` config — the GitHub Pages site retired by #235.

These are cheap to reintroduce and invisible without rendering the page in a
browser, since ReSpec builds the header client-side.
"""

from __future__ import annotations

import re
import unittest
from pathlib import Path

REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
SPEC_TEMPLATE = REPOSITORY_ROOT / "respec" / "template.html"
ARCHIVE_SPEC = (
REPOSITORY_ROOT / "site" / "public" / "spec" / "archive" / "1.0" / "index.html"
)

#: Hosts and paths known not to resolve.
RETIRED_URLS = (
"ekgf.github.io/dprod",
"https://www.omg.org/spec/DPROD/dprod.ttl",
)
SPEC_BASE = "https://ekgf.org/dprod/spec/"


class SpecHeaderUrlTest(unittest.TestCase):
@staticmethod
def without_comments(content: str) -> str:
"""Strip comments: a URL explained in one is not a URL that is linked."""
content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
return "\n".join(
line for line in content.splitlines() if not line.lstrip().startswith("//")
)

def documents(self) -> list[tuple[str, str]]:
return [
(
path.relative_to(REPOSITORY_ROOT).as_posix(),
self.without_comments(path.read_text("utf-8")),
)
for path in (SPEC_TEMPLATE, ARCHIVE_SPEC)
]

def test_no_retired_urls_are_linked(self) -> None:
for name, content in self.documents():
for retired in RETIRED_URLS:
with self.subTest(document=name, url=retired):
self.assertNotIn(
retired,
content,
f"{name} references {retired}, which does not resolve.",
)

def test_both_documents_set_an_editors_draft(self) -> None:
"""Unset, ReSpec infers the retired GitHub Pages URL from `github`."""
for name, content in self.documents():
with self.subTest(document=name):
self.assertIn(
f'edDraftURI: "{SPEC_BASE}develop/"',
content,
f"{name} must set edDraftURI explicitly to the canonical "
f"develop draft.",
)

def test_latest_version_is_the_omg_catalog(self) -> None:
for name, content in self.documents():
with self.subTest(document=name):
self.assertIn(
'latestVersion: "https://www.omg.org/spec/DPROD/"',
content,
f"{name}: latestVersion must be the OMG catalog entry, which "
f"always resolves to the newest published version.",
)

def test_this_version_carries_the_basepath_and_a_slug(self) -> None:
template = SPEC_TEMPLATE.read_text(encoding="utf-8")
self.assertIn(
f'thisVersion: "{SPEC_BASE}{{{{ branch_slug }}}}/"',
template,
"thisVersion must use the /dprod basePath and the slugified branch; "
"a raw branch name containing a slash does not resolve.",
)

archive = ARCHIVE_SPEC.read_text(encoding="utf-8")
self.assertIn(f'thisVersion: "{SPEC_BASE}main/"', archive)

def test_no_href_swallows_a_trailing_full_stop(self) -> None:
"""`href="....htm."` — the sentence's full stop inside the URL."""
for name, content in self.documents():
with self.subTest(document=name):
offenders = re.findall(r'href="[^"]*\.(?:htm|html|org|com)\."', content)
self.assertEqual([], offenders, f"{name}: {offenders}")


if __name__ == "__main__":
unittest.main()
Loading