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
+14
View File
@@ -5,6 +5,7 @@
"A difference between any two implementations is a note that changes shape when it syncs. Add a case here, not to one suite.",
"Lives under core/ because core is the definition, and because core/ is in the desktop, Android and server file sets, so changing a case reruns every suite that reads it.",
"display_titles: a note's name is its first line that says anything once a task marker is stripped, cut to 200 characters (not bytes). Lines split on \\n alone, as the grammar does everywhere.",
"urls: the links in a body the server unfurls (found), and the one a note that is nothing but a link files its preview under (lone). Trailing .,;:!? is the sentence's, not the link's, on every surface.",
"recurrences: the repeat rules a reminder can carry, as stored. The server, the core and the web's editor each hold the list; Android's picker pins it by hand, as it does the tint.",
"Tag cases are plain text on purpose. The web's renderer lets `code` and **bold** claim a `#` first, which the derivers do not; that is a rendering choice, not grammar, and is tested where it lives."
],
@@ -82,6 +83,19 @@
{ "body": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "title": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" }
],
"urls": [
{ "body": "https://example.com/a", "found": ["https://example.com/a"], "lone": "https://example.com/a" },
{ "body": " https://example.com/a \n", "found": ["https://example.com/a"], "lone": "https://example.com/a" },
{ "body": "https://example.com/a.", "found": ["https://example.com/a"], "lone": "https://example.com/a" },
{ "body": "https://example.com/a?q=1!", "found": ["https://example.com/a?q=1"], "lone": "https://example.com/a?q=1" },
{ "body": "see https://example.com/a, then https://example.com/b.", "found": ["https://example.com/a", "https://example.com/b"], "lone": null },
{ "body": "<https://example.com/a>", "found": ["https://example.com/a"], "lone": null },
{ "body": "https://example.com/a)", "found": ["https://example.com/a"], "lone": null },
{ "body": "https://example.com/a https://example.com/a", "found": ["https://example.com/a"], "lone": null },
{ "body": "ftp://example.com/a", "found": [], "lone": null },
{ "body": "", "found": [], "lone": null }
],
"recurrences": ["daily", "weekly", "monthly", "yearly"],
"tint": {