A lone link's preview is looked up where the server filed it

The server unfurls what detect_urls finds, trailing .,;:!? trimmed, and files the
preview under that. The web and Android cards looked a lone link's preview up
under body.trim(), punctuation included, so a note reading
"https://example.com/a." never showed its card.

- grammar.json gains a urls section: what the server finds in a body, and the
  link a lone-link note is filed under.
- The web's rule moves out of NoteCard into notes/links.ts loneUrl(); Android's
  LinkPreviewRow gets the same loneUrl(); both trim like the server.
- The server and web suites run the cases; Android's JVM test pins them by hand,
  as it does the tint.

Fixes #5399. DRY pass #2, batch 3 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:30:32 -04:00
co-authored by Claude Opus 5.5
parent 3766d59b2e
commit 1c4bf56058
7 changed files with 99 additions and 13 deletions
+7
View File
@@ -8,6 +8,7 @@ import { describe, expect, it } from "vitest";
import fixture from "../../../core/testdata/grammar.json";
import { DERIVED_TINT_KEYS, derivedTint, resolveLabelColor, tintHash } from "./colors";
import { parseInline, parseTaskLine, renderTaskLine } from "./markdown";
import { loneUrl } from "./links";
import { RECURRENCES } from "./recurrence";
describe("checklist lines", () => {
@@ -59,3 +60,9 @@ describe("recurrences", () => {
expect(RECURRENCES.map((r) => r.value)).toEqual(fixture.recurrences);
});
});
describe("links", () => {
it.each(fixture.urls)("lone link in $body", ({ body, lone }) => {
expect(loneUrl(body)).toBe(lone);
});
});
+17
View File
@@ -0,0 +1,17 @@
// A note that is nothing but a link renders as a link card.
//
// The characters a link runs over, and the punctuation trimmed off its end, are the
// server's (unfurl_queue.detect_urls): the server files the preview under the trimmed
// link, so the card has to look it up under the same one (#5399). Android's
// LinkPreviewRow holds the same rule, and all three run the `urls` cases in
// core/testdata/grammar.json.
//
// Whitespace either side still counts as lone: someone pasting a link rarely trims it.
const LONE_URL_RE = /^\s*(https?:\/\/[^\s<>"'\])]+)\s*$/;
/** The link a lone-link note's preview is filed under, or null for any other body. */
export function loneUrl(body: string): string | null {
const match = LONE_URL_RE.exec(body);
if (!match) return null;
return match[1].replace(/[.,;:!?]+$/, "") || null;
}