From 6eef671f1403bc3246c3395743d0eb102b3774ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 4 Oct 2026 21:06:31 -0400 Subject: [PATCH] Catalog: an address passes when an allowed one leads it and only punctuation follows Co-Authored-By: Claude Opus 5.5 --- scripts/catalog.mjs | 10 +--------- tests/catalog.test.mjs | 7 +++++++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/catalog.mjs b/scripts/catalog.mjs index 06f28bb..2d5a27e 100644 --- a/scripts/catalog.mjs +++ b/scripts/catalog.mjs @@ -37,14 +37,6 @@ function time(value) { return zone && at.getUTCMonth() === mo - 1 && at.getUTCDate() === d && h < 24 && mi < 60 && sec < 60 && !Number.isNaN(Date.parse(value)); } -/** An address as written in prose, less the punctuation that ends a sentence. */ -function trim(url) { - let out = url.replace(/[.,;:!?*_~]+$/, ""); - const count = (c) => out.split(c).length - 1; - while (out.endsWith(")") && count(")") > count("(")) out = out.slice(0, -1).replace(/[.,;:!?*_~]+$/, ""); - return out; -} - function fail(where, what) { throw new Error(`${where}: ${what}`); } @@ -156,7 +148,7 @@ function readme(dir, app) { const markdown = bytes.toString("utf8"); const links = [app.website, app.source].filter(Boolean); if (ENTITY.test(markdown)) fail(where, `write characters as they are, not as references: ${markdown.match(ENTITY)[0]}`); - for (const url of markdown.replaceAll("\\", "").match(URLISH) ?? []) if (!links.includes(trim(url))) fail(where, `addresses go only to the website or source: ${url}`); + for (const url of markdown.replaceAll("\\", "").match(URLISH) ?? []) if (!links.some((l) => url.startsWith(l) && /^[.,;:!?*_~)]*$/.test(url.slice(l.length)))) fail(where, `addresses go only to the website or source: ${url}`); const walk = (tokens) => { for (const t of tokens) { if (!TOKENS.has(t.type)) fail(where, `${t.type === "html" ? "raw HTML" : t.type === "code" ? "a code block" : t.type} is not allowed: ${t.raw.trim().slice(0, 60)}`); diff --git a/tests/catalog.test.mjs b/tests/catalog.test.mjs index 201b405..5fc055e 100644 --- a/tests/catalog.test.mjs +++ b/tests/catalog.test.mjs @@ -65,6 +65,12 @@ test("addresses in prose may end a sentence or hold parentheses", () => { assert.equal(read(catalog({ app, files: { "README.md": readme } })).apps.length, 1); }); +test("an address may end in punctuation of its own", () => { + const app = { ...good, source: "https://github.com/ada/tally_", website: "https://tally.app/?" }; + const readme = "[Source](https://github.com/ada/tally_) and [site](https://tally.app/?).\n"; + assert.equal(read(catalog({ app, files: { "README.md": readme } })).apps.length, 1); +}); + test("the empty catalog reads", () => { const root = mkdtempSync(join(tmpdir(), "showcase-")); mkdirSync(join(root, "apps")); @@ -115,6 +121,7 @@ const bad = [ ["a picture outside the folder in a README", { files: { "README.md": "![x](../a.png)\n" } }, /lowercase .png file name/], ["inline code in a README", { files: { "README.md": "Run `cargo install tally`.\n" } }, /codespan is not allowed/], ["a character reference in a README", { files: { "README.md": "Go https://evil.io/ now\n" } }, /not as references/], + ["an address that runs on past the website", { files: { "README.md": "See https://github.com/ada/tally.evil.io now\n" } }, /addresses go only/], ["an escaped address in a README", { files: { "README.md": "Go https\://evil.io/ now\n" } }, /addresses go only/], ["a large README", { files: { "README.md": "a".repeat(10241) } }, /over 10240/], ];