Skip to content

Commit 53a6776

Browse files
Update option to verify broken links
1 parent cb778d7 commit 53a6776

6 files changed

Lines changed: 234 additions & 32 deletions

File tree

.github/workflows/example-executability.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,11 @@ jobs:
2828
BASELINE_VERSION=$(python -c 'import pathlib, re; text = pathlib.Path("README.md").read_text(encoding="utf-8"); print(re.search(r"This repo is aligned with `multilingualprogramming` `([^`]+)`\.", text).group(1))')
2929
python -m pip install "multilingualprogramming==${BASELINE_VERSION}"
3030
31+
- name: Validate release metadata
32+
run: python tools/check_release.py
33+
34+
- name: Check local links
35+
run: python tools/check_links.py
36+
3137
- name: Execute all example programs
3238
run: python tools/check_examples.py

CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ First tagged release of the browser playground.
1010

1111
### Added
1212

13-
- Release validation via [`tools/check_release.py`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/tools/check_release.py).
14-
- Example executability validation via [`tools/check_examples.py`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/tools/check_examples.py) and GitHub Actions in [`.github/workflows/example-executability.yml`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/.github/workflows/example-executability.yml).
13+
- Release validation via [`tools/check_release.py`](tools/check_release.py).
14+
- Example executability validation via [`tools/check_examples.py`](tools/check_examples.py) and GitHub Actions in [`.github/workflows/example-executability.yml`](.github/workflows/example-executability.yml).
1515
- Keyboard-accessible output tabs and split-pane resizing.
16-
- Release process documentation in [`RELEASE.md`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/RELEASE.md) and contribution guidance in [`CONTRIBUTING.md`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/CONTRIBUTING.md).
16+
- Release process documentation in [`RELEASE.md`](RELEASE.md) and contribution guidance in [`CONTRIBUTING.md`](CONTRIBUTING.md).
1717

1818
### Changed
1919

2020
- Aligned the playground with `multilingualprogramming` `0.6.0`.
2121
- Highlighted broader browser-facing WAT/WASM coverage for classes, control flow, async features, properties, and bytes literals.
22-
- Kept the page shell in [`index.html`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/index.html) and organized the client logic across [`assets/main.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/main.js), [`assets/editor.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/editor.js), [`assets/runtime.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/runtime.js), [`assets/ui.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/ui.js), and [`assets/examples.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/examples.js).
22+
- Kept the page shell in [`index.html`](index.html) and organized the client logic across [`assets/main.js`](assets/main.js), [`assets/editor.js`](assets/editor.js), [`assets/runtime.js`](assets/runtime.js), [`assets/ui.js`](assets/ui.js), and [`assets/examples.js`](assets/examples.js).
2323
- Improved keyboard focus handling, live status announcements, and narrow-screen behavior.
2424
- Normalized source comments and visible UI copy to reduce encoding-risky decorative characters.
2525

CONTRIBUTING.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,23 @@ This repository is a static browser playground for the Multilingual Programming
66

77
## Before you change anything
88

9-
1. Read [`README.md`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/README.md).
9+
1. Read [`README.md`](README.md).
1010
2. Keep this repo aligned with the current target `multilingualprogramming` version.
1111
3. Prefer small, reviewable changes over broad rewrites.
1212

1313
## Project structure
1414

15-
- [`index.html`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/index.html): page shell and static markup
16-
- [`assets/playground.css`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/playground.css): site styling
17-
- [`assets/playground.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/playground.js): runtime logic, examples, and UI behavior
18-
- [`tools/check_release.py`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/tools/check_release.py): release validation
15+
- [`index.html`](index.html): page shell and static markup
16+
- [`assets/playground.css`](assets/playground.css): site styling
17+
- [`assets/main.js`](assets/main.js), [`assets/editor.js`](assets/editor.js), [`assets/runtime.js`](assets/runtime.js), [`assets/ui.js`](assets/ui.js), [`assets/theme.js`](assets/theme.js), [`assets/i18n.js`](assets/i18n.js), [`assets/examples.js`](assets/examples.js): runtime logic, examples, and UI behavior
18+
- [`tools/check_release.py`](tools/check_release.py): release validation
19+
- [`tools/check_examples.py`](tools/check_examples.py): example executability validation
1920

2021
## Development expectations
2122

2223
- Preserve the browser-only deployment model. Do not introduce a server dependency unless explicitly intended.
23-
- Keep language options and examples aligned. If you add or remove a supported language, update both the selector in [`index.html`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/index.html) and the example maps in [`assets/playground.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/playground.js).
24-
- Keep metadata coherent. If copy or publish URLs change, review [`sitemap.xml`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/sitemap.xml), [`robots.txt`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/robots.txt), and [`site.webmanifest`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/site.webmanifest).
24+
- Keep language options and examples aligned. If you add or remove a supported language, update both the selector in [`index.html`](index.html) and the example maps in [`assets/examples.js`](assets/examples.js).
25+
- Keep metadata coherent. If copy or publish URLs change, review [`sitemap.xml`](sitemap.xml), [`robots.txt`](robots.txt), and [`site.webmanifest`](site.webmanifest).
2526
- Prefer ASCII in comments and UI chrome unless non-ASCII text is user content, a language example, or a real requirement.
2627

2728
## Validation
@@ -44,8 +45,8 @@ Also do a manual browser pass on:
4445

4546
## Changelog and release docs
4647

47-
- Update [`CHANGELOG.md`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/CHANGELOG.md) for user-visible or maintenance-significant changes.
48-
- Use [`RELEASE.md`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/RELEASE.md) when preparing a release summary or publish checklist.
48+
- Update [`CHANGELOG.md`](CHANGELOG.md) for user-visible or maintenance-significant changes.
49+
- Use [`RELEASE.md`](RELEASE.md) when preparing a release summary or publish checklist.
4950

5051
## Pull request guidance
5152

RELEASE.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,22 @@ Prepare a playground release when at least one of these changes:
1717
## Release checklist
1818

1919
1. Confirm the target `multilingualprogramming` version.
20-
2. Update [`README.md`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/README.md) baseline text if the aligned version changed.
21-
3. Update visible release copy in [`index.html`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/index.html), especially the backend note in the WAT / WASM tab.
22-
4. Refresh examples and language metadata in [`assets/examples.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/examples.js), [`assets/editor.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/editor.js), and related runtime modules if language coverage changed.
20+
2. Update [`README.md`](README.md) baseline text if the aligned version changed.
21+
3. Update visible release copy in [`index.html`](index.html), especially the backend note in the WAT / WASM tab.
22+
4. Refresh examples and language metadata in [`assets/examples.js`](assets/examples.js), [`assets/editor.js`](assets/editor.js), and related runtime modules if language coverage changed.
2323
5. Verify metadata files:
24-
[`sitemap.xml`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/sitemap.xml),
25-
[`robots.txt`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/robots.txt),
26-
[`site.webmanifest`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/site.webmanifest).
24+
[`sitemap.xml`](sitemap.xml),
25+
[`robots.txt`](robots.txt),
26+
[`site.webmanifest`](site.webmanifest).
2727
6. Run:
2828
`python tools/check_release.py --expected-date YYYY-MM-DD`
2929
7. Run:
3030
`python tools/check_examples.py`
3131
8. Confirm GitHub Actions passes:
32-
[`.github/workflows/example-executability.yml`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/.github/workflows/example-executability.yml)
33-
9. Open [`index.html`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/index.html) in a browser and smoke-test:
32+
[`.github/workflows/example-executability.yml`](.github/workflows/example-executability.yml)
33+
9. Open [`index.html`](index.html) in a browser and smoke-test:
3434
language switching, example switching, run/clear/share, output tabs, theme toggle, keyboard navigation, and narrow-screen layout.
35-
10. Update [`CHANGELOG.md`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/CHANGELOG.md).
35+
10. Update [`CHANGELOG.md`](CHANGELOG.md).
3636
11. Prepare outward-facing release notes using the template below.
3737

3838
## Release note template
@@ -71,7 +71,7 @@ Highlights in the current release line:
7171
- stateful classes and inherited method dispatch
7272
- `with`, `try/except`, `lambda`, `match/case`, and `async/await`
7373
- `@property` accessors and bytes literals
74-
- modular browser assets centered on [`assets/playground.css`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/playground.css), [`assets/main.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/main.js), and [`assets/runtime.js`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/assets/runtime.js)
74+
- modular browser assets centered on [`assets/playground.css`](assets/playground.css), [`assets/main.js`](assets/main.js), and [`assets/runtime.js`](assets/runtime.js)
7575
- improved accessibility and mobile behavior
76-
- added release validation with [`tools/check_release.py`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/tools/check_release.py)
77-
- added example executability validation with [`tools/check_examples.py`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/tools/check_examples.py) and [`.github/workflows/example-executability.yml`](/c:/Users/john.samuel/Documents/Research/Workspace/playground/.github/workflows/example-executability.yml)
76+
- added release validation with [`tools/check_release.py`](tools/check_release.py)
77+
- added example executability validation with [`tools/check_examples.py`](tools/check_examples.py) and [`.github/workflows/example-executability.yml`](.github/workflows/example-executability.yml)

tools/check_links.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#
2+
# SPDX-FileCopyrightText: 2026 John Samuel <johnsamuelwrites@gmail.com>
3+
#
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
#
6+
7+
from __future__ import annotations
8+
9+
import json
10+
import re
11+
import sys
12+
from html.parser import HTMLParser
13+
from pathlib import Path
14+
from urllib.parse import urlsplit
15+
16+
17+
ROOT = Path(__file__).resolve().parents[1]
18+
MARKDOWN_PATHS = sorted(ROOT.glob("*.md"))
19+
HTML_PATHS = [ROOT / "index.html"]
20+
MANIFEST_PATH = ROOT / "site.webmanifest"
21+
MARKDOWN_LINK_RE = re.compile(r"\[[^\]]+\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
22+
23+
24+
def is_external(target: str) -> bool:
25+
return target.startswith(("http://", "https://", "mailto:", "tel:", "javascript:")) or target == ""
26+
27+
28+
def normalize_manifest_path(target: str) -> str:
29+
if target.startswith("/playground/"):
30+
return target.removeprefix("/playground/")
31+
if target.startswith("/"):
32+
return target.lstrip("/")
33+
return target
34+
35+
36+
def resolve_local_target(target: str, source_path: Path) -> tuple[Path | None, str | None]:
37+
parsed = urlsplit(target)
38+
raw_path = parsed.path
39+
fragment = parsed.fragment or None
40+
41+
if not raw_path:
42+
return source_path, fragment
43+
44+
if source_path == MANIFEST_PATH:
45+
raw_path = normalize_manifest_path(raw_path)
46+
47+
if raw_path.startswith("/"):
48+
candidate = ROOT / raw_path.lstrip("/")
49+
else:
50+
candidate = source_path.parent / raw_path
51+
52+
return candidate.resolve(), fragment
53+
54+
55+
def path_exists(candidate: Path) -> bool:
56+
return candidate.exists() or candidate.is_dir()
57+
58+
59+
def heading_slug(text: str) -> str:
60+
slug = text.strip().lower()
61+
slug = re.sub(r"[`*_]+", "", slug)
62+
slug = re.sub(r"[^\w\s-]", "", slug, flags=re.UNICODE)
63+
slug = re.sub(r"\s+", "-", slug, flags=re.UNICODE)
64+
slug = re.sub(r"-{2,}", "-", slug).strip("-")
65+
return slug
66+
67+
68+
def collect_markdown_anchors(path: Path) -> set[str]:
69+
anchors: set[str] = set()
70+
for line in path.read_text(encoding="utf-8").splitlines():
71+
match = re.match(r"^(#{1,6})\s+(.*)$", line.strip())
72+
if not match:
73+
continue
74+
slug = heading_slug(match.group(2))
75+
if slug:
76+
anchors.add(slug)
77+
return anchors
78+
79+
80+
class LinkHTMLParser(HTMLParser):
81+
def __init__(self) -> None:
82+
super().__init__()
83+
self.links: list[str] = []
84+
self.anchors: set[str] = set()
85+
86+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
87+
attr_map = dict(attrs)
88+
89+
if tag == "a" and attr_map.get("href"):
90+
self.links.append(attr_map["href"])
91+
if tag == "link" and attr_map.get("href"):
92+
self.links.append(attr_map["href"])
93+
if tag in {"script", "img"} and attr_map.get("src"):
94+
self.links.append(attr_map["src"])
95+
96+
for key in ("id", "name"):
97+
value = attr_map.get(key)
98+
if value:
99+
self.anchors.add(value)
100+
101+
102+
def collect_html_anchors(path: Path) -> set[str]:
103+
parser = LinkHTMLParser()
104+
parser.feed(path.read_text(encoding="utf-8"))
105+
return parser.anchors
106+
107+
108+
def collect_html_links(path: Path) -> list[str]:
109+
parser = LinkHTMLParser()
110+
parser.feed(path.read_text(encoding="utf-8"))
111+
return parser.links
112+
113+
114+
def extract_markdown_links(path: Path) -> list[str]:
115+
return [match.group(1) for match in MARKDOWN_LINK_RE.finditer(path.read_text(encoding="utf-8"))]
116+
117+
118+
def validate_target(target: str, source_path: Path, errors: list[str]) -> None:
119+
if is_external(target):
120+
return
121+
122+
if target.startswith("#"):
123+
fragment = target[1:]
124+
anchors = collect_markdown_anchors(source_path) if source_path.suffix == ".md" else collect_html_anchors(source_path)
125+
if fragment not in anchors:
126+
errors.append(f"{source_path.relative_to(ROOT)} -> missing local anchor #{fragment}")
127+
return
128+
129+
candidate, fragment = resolve_local_target(target, source_path)
130+
if candidate is None or not path_exists(candidate):
131+
errors.append(f"{source_path.relative_to(ROOT)} -> missing target {target}")
132+
return
133+
134+
if fragment:
135+
if candidate.suffix == ".md":
136+
anchors = collect_markdown_anchors(candidate)
137+
elif candidate.suffix in {".html", ".htm"}:
138+
anchors = collect_html_anchors(candidate)
139+
else:
140+
anchors = set()
141+
142+
if anchors and fragment not in anchors:
143+
errors.append(f"{source_path.relative_to(ROOT)} -> missing anchor #{fragment} in {candidate.relative_to(ROOT)}")
144+
145+
146+
def main() -> int:
147+
if hasattr(sys.stdout, "reconfigure"):
148+
sys.stdout.reconfigure(encoding="utf-8")
149+
if hasattr(sys.stderr, "reconfigure"):
150+
sys.stderr.reconfigure(encoding="utf-8")
151+
152+
errors: list[str] = []
153+
154+
for path in MARKDOWN_PATHS:
155+
for target in extract_markdown_links(path):
156+
validate_target(target, path, errors)
157+
158+
for path in HTML_PATHS:
159+
for target in collect_html_links(path):
160+
validate_target(target, path, errors)
161+
162+
manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
163+
for icon in manifest.get("icons", []):
164+
src = icon.get("src", "")
165+
if src:
166+
validate_target(src, MANIFEST_PATH, errors)
167+
168+
if errors:
169+
for error in errors:
170+
print(f"ERROR: {error}")
171+
return 1
172+
173+
print("Link checks passed.")
174+
print(f"Markdown files checked: {len(MARKDOWN_PATHS)}")
175+
print(f"HTML files checked: {len(HTML_PATHS)}")
176+
print(f"Manifest icons checked: {len(manifest.get('icons', []))}")
177+
return 0
178+
179+
180+
if __name__ == "__main__":
181+
raise SystemExit(main())

tools/check_release.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
MANIFEST_PATH = ROOT / "site.webmanifest"
2121
ROBOTS_PATH = ROOT / "robots.txt"
2222
SITEMAP_PATH = ROOT / "sitemap.xml"
23-
PLAYGROUND_JS_PATH = ROOT / "assets" / "playground.js"
23+
EXAMPLES_JS_PATH = ROOT / "assets" / "examples.js"
2424

2525

2626
def read_text(path: Path) -> str:
@@ -35,9 +35,9 @@ def expect_match(pattern: str, text: str, label: str) -> str:
3535

3636

3737
def parse_const_keys(script: str, const_name: str) -> set[str]:
38-
match = re.search(rf"const {const_name} = \{{(.*?)^\}};", script, re.MULTILINE | re.DOTALL)
38+
match = re.search(rf"export const {const_name} = \{{(.*?)^\}};", script, re.MULTILINE | re.DOTALL)
3939
if not match:
40-
raise ValueError(f"Could not parse {const_name} from assets/playground.js.")
40+
raise ValueError(f"Could not parse {const_name} from assets/examples.js.")
4141
return set(re.findall(r"^\s*([a-z]{2}):\s*`", match.group(1), re.MULTILINE))
4242

4343

@@ -60,7 +60,7 @@ def main() -> int:
6060
manifest = json.loads(read_text(MANIFEST_PATH))
6161
robots_text = read_text(ROBOTS_PATH)
6262
sitemap_root = ET.fromstring(read_text(SITEMAP_PATH))
63-
playground_js = read_text(PLAYGROUND_JS_PATH)
63+
examples_js = read_text(EXAMPLES_JS_PATH)
6464
except Exception as exc:
6565
print(f"release check failed to read repository files: {exc}", file=sys.stderr)
6666
return 1
@@ -128,9 +128,16 @@ def main() -> int:
128128

129129
for required_rel_path in [
130130
Path("assets/playground.css"),
131-
Path("assets/playground.js"),
131+
Path("assets/main.js"),
132+
Path("assets/editor.js"),
133+
Path("assets/runtime.js"),
134+
Path("assets/ui.js"),
135+
Path("assets/theme.js"),
136+
Path("assets/i18n.js"),
137+
Path("assets/examples.js"),
132138
Path("assets/images/og-image.svg"),
133139
Path("assets/images/logo.svg"),
140+
Path("assets/images/favicon.svg"),
134141
Path("apple-touch-icon.png"),
135142
Path("favicon.ico"),
136143
]:
@@ -144,15 +151,22 @@ def main() -> int:
144151
errors.append(f"Manifest icon source does not exist: {src}.")
145152

146153
language_options = set(re.findall(r'<option value="([a-z]{2})">', index_text))
147-
basic_examples = parse_const_keys(playground_js, "EXAMPLES")
148-
advanced_examples = parse_const_keys(playground_js, "EXAMPLES_ADVANCED")
154+
basic_examples = parse_const_keys(examples_js, "EXAMPLES")
155+
medium_examples = parse_const_keys(examples_js, "EXAMPLES_MEDIUM")
156+
advanced_examples = parse_const_keys(examples_js, "EXAMPLES_ADVANCED")
149157

150158
if language_options != basic_examples:
151159
errors.append(
152160
"Language dropdown values do not match EXAMPLES keys: "
153161
f"options={sorted(language_options)}, examples={sorted(basic_examples)}."
154162
)
155163

164+
if language_options != medium_examples:
165+
errors.append(
166+
"Language dropdown values do not match EXAMPLES_MEDIUM keys: "
167+
f"options={sorted(language_options)}, medium={sorted(medium_examples)}."
168+
)
169+
156170
if language_options != advanced_examples:
157171
errors.append(
158172
"Language dropdown values do not match EXAMPLES_ADVANCED keys: "

0 commit comments

Comments
 (0)