Skip to content

Commit ba8bf6e

Browse files
TMHSDigitalclaude
andcommitted
site: renderer and link fixes from the entry pages review
- Deprecation banners start the reason with a capital letter. - Bare URLs stop at bold markers and code spans, keep a closing paren that balances one inside them, and <url> autolinks render as links. - A malformed percent escape in a body link is a named link error, not a crash; a ?query on a relative link is kept. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wc67NX9MM964uaVGn4bpZJ
1 parent 849af07 commit ba8bf6e

3 files changed

Lines changed: 37 additions & 9 deletions

File tree

‎site/lib/lib.test.mjs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ eq("split rest starts at first h2", splitEntryBody(body).rest.split("\n")[0], "#
8989
eq("split restFirstLine", splitEntryBody(body).restFirstLine, 7);
9090
eq("sectionMarkdown Notes", sectionMarkdown(body, "Notes"), "- n1\n- Deprecated: gone");
9191
eq("sectionMarkdown missing", sectionMarkdown(body, "Nope"), null);
92-
eq("deprecationReason", deprecationReason(body), "gone");
92+
eq("deprecationReason starts a sentence", deprecationReason(body), "Gone");
9393

9494
/* links -------------------------------------------------------------------- */
9595
import { makeLinkResolver } from "./links.mjs";
@@ -323,6 +323,15 @@ const lfs = llmsFullTxt({ entries: llmsEntries, site, categories: cats, bodies:
323323
has("full lists each pick", lfs, "- Tiles: Alpha (https://x.test/s/entry/a1/); CC0; commercial OK; no credit required; active");
324324
has("full lists credits", lfs, "Credits to ship: none");
325325

326+
/* deferred minors, batch A ------------------------------------------------- */
327+
eq("deprecation reason is capitalised", deprecationReason("- Deprecated: the service is offline"), "The service is offline");
328+
eq("bare url in bold", renderInline("**https://a.test/x**", passLink), '<strong><a href="https://a.test/x" rel="noopener noreferrer">https://a.test/x</a></strong>');
329+
eq("bare url keeps a balanced paren", renderInline("see https://a.test/wiki/Foo_(bar).", passLink), 'see <a href="https://a.test/wiki/Foo_(bar)" rel="noopener noreferrer">https://a.test/wiki/Foo_(bar)</a>.');
330+
eq("angle-bracket autolink", renderInline("<https://a.test/x>", passLink), '<a href="https://a.test/x" rel="noopener noreferrer">https://a.test/x</a>');
331+
eq("bare url stops at a code span", renderInline("https://a.test/x`y`", passLink), '<a href="https://a.test/x" rel="noopener noreferrer">https://a.test/x</a><code>y</code>');
332+
throws("malformed escape is a link error", () => resolve("bad%E0.md"), 'link target "bad%E0.md" is malformed');
333+
eq("query on a relative link is kept", resolve("../../docs/provenance.md?plain=1").href, "https://github.com/o/r/blob/main/docs/provenance.md?plain=1");
334+
326335
/* report (keep last) ------------------------------------------------------ */
327336
if (failures.length) {
328337
console.error(`lib.test failed (${failures.length}):`);

‎site/lib/links.mjs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ export function makeLinkResolver({ entryPath, idByPath, repo, kindOf, entryBase
88
return (raw) => {
99
if (/^(https?:|mailto:)/i.test(raw)) return { href: raw, external: true };
1010
if (raw.startsWith("#")) return { href: raw, external: false };
11-
const at = raw.indexOf("#");
12-
const target = at === -1 ? raw : raw.slice(0, at);
13-
const hash = at === -1 ? "" : raw.slice(at);
14-
const repoPath = path.posix.normalize(path.posix.join(dir, decodeURI(target))).replace(/\/$/, "");
11+
// Split off ?query and #hash; both are kept on the rewritten link.
12+
const m = raw.match(/^([^?#]*)(\?[^#]*)?(#.*)?$/);
13+
const target = m[1];
14+
const hash = `${m[2] || ""}${m[3] || ""}`;
15+
let decoded;
16+
try {
17+
decoded = decodeURI(target);
18+
} catch {
19+
throw new LinkError(`${entryPath}: link target "${raw}" is malformed`);
20+
}
21+
const repoPath = path.posix.normalize(path.posix.join(dir, decoded)).replace(/\/$/, "");
1522
const id = idByPath.get(repoPath);
1623
if (id) return { href: `${entryBase}${id}/${hash}`, external: false };
1724
const kind = repoPath.startsWith("..") ? null : kindOf(repoPath);

‎site/lib/markdown.mjs‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,18 @@ export function renderInline(text, resolveLink) {
3535
const rel = external ? ' rel="noopener noreferrer"' : "";
3636
return keep(`<a href="${esc(out)}"${rel}>${marks(esc(label))}</a>`);
3737
});
38-
// Bare URLs, leaving trailing sentence punctuation outside the link.
39-
s = s.replace(/https?:\/\/[^\s<>"]+/g, (url) => {
40-
const [, core, tail] = url.match(/^(.*?)([.,;:!?)\]]*)$/);
38+
// Autolinks in angle brackets.
39+
s = s.replace(/<(https?:\/\/[^\s<>]+)>/g, (_, url) => keep(`<a href="${esc(url)}" rel="noopener noreferrer">${esc(url)}</a>`));
40+
// Bare URLs, leaving trailing sentence punctuation outside the link. A URL
41+
// stops at `*` (bold around it) and at a slot (a code span right after it),
42+
// and keeps a closing paren that balances one inside it (Foo_(bar)).
43+
s = s.replace(/https?:\/\/[^\s<>"*\u0000]+/g, (url) => {
44+
let [, core, tail] = url.match(/^(.*?)([.,;:!?)\]]*)$/);
45+
const count = (str, ch) => str.split(ch).length - 1;
46+
while (tail.startsWith(")") && count(core, "(") > count(core, ")")) {
47+
core += ")";
48+
tail = tail.slice(1);
49+
}
4150
return keep(`<a href="${esc(core)}" rel="noopener noreferrer">${esc(core)}</a>`) + tail;
4251
});
4352
s = marks(esc(s));
@@ -161,5 +170,8 @@ export function sectionMarkdown(body, heading) {
161170
/** The text after the `- Deprecated:` marker line, or null. */
162171
export function deprecationReason(body) {
163172
const m = String(body).match(/^- Deprecated:\s*(.+)$/m);
164-
return m ? m[1].trim() : null;
173+
if (!m) return null;
174+
// The banner reads "Deprecated. <reason>", so the reason starts a sentence.
175+
const reason = m[1].trim();
176+
return reason.charAt(0).toUpperCase() + reason.slice(1);
165177
}

0 commit comments

Comments
 (0)