diff --git a/datam8-model b/datam8-model
index 591b796..002aa65 160000
--- a/datam8-model
+++ b/datam8-model
@@ -1 +1 @@
-Subproject commit 591b796a962117ef2a1f9d807074bf3a4f5cc159
+Subproject commit 002aa65f6f127f91d06c8dc0fc4ea50ce75616fc
diff --git a/pyproject.toml b/pyproject.toml
index 2608a9d..50515bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,8 +47,8 @@ Issues = "https://github.com/oraylis/datam8/issues"
[dependency-groups]
dev = [
- "pytest~=8.3.0", # TODO: upgrade to 9.x.x
- "pytest-cases~=3.8.0",
+ "pytest~=9.0.3",
+ "pytest-cases~=3.10.1",
"coverage~=7.13.5",
"hatchling~=1.27.0",
"httpx~=0.25.0",
@@ -61,6 +61,8 @@ build-backend = "hatchling.build"
[tool.uv]
exclude-newer = "1 weeks"
+# Allow critical CA trust-store fixes without waiting for the global cooldown (GHSA-6q48-6c7w-9gw2).
+exclude-newer-package = { certifi = false }
#############
# tool.ruff #
diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs
index d7af701..d1e0a69 100644
--- a/scripts/check-docs.mjs
+++ b/scripts/check-docs.mjs
@@ -23,7 +23,14 @@ export function headingAnchors(source) {
for (const line of proseLines(source)) {
const heading = line.match(/^#{1,6}\s+(.+?)\s*#*$/)?.[1];
if (!heading) continue;
- const slug = heading.toLowerCase().replace(/<[^>]*>/g, "")
+ let text = heading.toLowerCase();
+ let previous;
+ // Remove innermost tags until stripping cannot expose another tag.
+ do {
+ previous = text;
+ text = text.replace(/<[^<>]*>/g, "");
+ } while (text !== previous);
+ const slug = text
.replace(/[^\p{L}\p{N}\p{M}_\-\s]/gu, "").replace(/\s/g, "-");
const count = counts.get(slug) || 0;
counts.set(slug, count + 1);
diff --git a/scripts/check-docs.test.mjs b/scripts/check-docs.test.mjs
index 03136e7..7200d39 100644
--- a/scripts/check-docs.test.mjs
+++ b/scripts/check-docs.test.mjs
@@ -25,6 +25,19 @@ test("reports empty alt text and heading spacing", () => {
assert.equal(errors.length, 2);
});
+test("heading anchors remove inline and reintroduced HTML tags", () => {
+ const source = "# Guide\n\n## Überblick 中文\n\n## Save\n\n## Save\n\n## t>Read\n\n## is removedt>alert(123)\n";
+ assert.deepEqual([...headingAnchors(source)], [
+ "guide", "überblick-中文", "save", "save-1", "read", "alert123",
+ ]);
+});
+
+test("heading anchors handle exposed and incomplete tags", () => {
+ assert.ok(headingAnchors("# ipt>Save\n").has("save"));
+ assert.ok(headingAnchors("# <em>Save\n").has("save"));
+ assert.ok(headingAnchors("#