From a21b2d2162f5c395f249ffb7c561533f044d5cdf Mon Sep 17 00:00:00 2001 From: JP Hastings-Spital Date: Sat, 27 Jun 2026 19:38:08 +0200 Subject: [PATCH] Show printed page number on book notes When a note's selector is refined by a physical-books FragmentSelector, render the page (or range) after the book title, e.g. "There Is No Antimemetics Division (p.255-256)". --- web/src/components/common/Card.tsx | 71 ++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/web/src/components/common/Card.tsx b/web/src/components/common/Card.tsx index 1631321..bfea650 100644 --- a/web/src/components/common/Card.tsx +++ b/web/src/components/common/Card.tsx @@ -46,6 +46,7 @@ import type { AnnotationItem, ContentLabel, LabelVisibility, + Selector, } from "../../types"; import { asTextQuote } from "../../types"; @@ -106,6 +107,40 @@ function getContentWarning( return null; } +const PHYSICAL_BOOKS_SPEC = "https://margin.at/docs/specs/physical-books"; + +// Printed page label from a selector's physical-books FragmentSelector, e.g. +// "255-256", or "A-1 - A-2" when a label itself contains an (encoded) hyphen. +function bookPageLabel(selector?: Selector | null): string | null { + for ( + let sel: Selector | null | undefined = selector; + sel; + sel = sel.refinedBy + ) { + if ( + sel.type !== "FragmentSelector" || + sel.conformsTo !== PHYSICAL_BOOKS_SPEC || + !sel.value + ) { + continue; + } + // Keep the value raw (%2D still encoded) so the range hyphen stays unambiguous. + const raw = sel.value + .split("&") + .map((pair) => pair.split("=")) + .find(([key]) => key === "page")?.[1]; + if (!raw) return null; + try { + const pages = raw.split("-").map(decodeURIComponent); + const separator = /%2d/i.test(raw) ? " - " : "-"; + return pages.join(separator); + } catch { + return null; + } + } + return null; +} + interface CardProps { item: AnnotationItem; onDelete?: (uri: string) => void; @@ -345,6 +380,7 @@ export default function Card({ const bookUrl = isbn ? `https://openlibrary.org/isbn/${isbn}` : null; const bookTitle = item.target?.title || item.title || (isbn ? `ISBN ${isbn}` : null); + const bookPage = bookPageLabel(item.target?.selector); const quoteLinkUrl = (() => { const sel = asTextQuote(item.target?.selector); @@ -566,20 +602,29 @@ export default function Card({ {pageUrl && !isBookmark && !(contentWarning && !contentRevealed) && ( - handleExternalClick(e, bookUrl || pageUrl)} - className="inline-flex items-center gap-1 text-xs text-primary-600 dark:text-primary-400 hover:underline mt-0.5 max-w-full" - > - {isbn ? ( - - ) : ( - +
+ handleExternalClick(e, bookUrl || pageUrl)} + className="inline-flex items-center gap-1 text-xs text-primary-600 dark:text-primary-400 hover:underline min-w-0" + > + {isbn ? ( + + ) : ( + + )} + + {isbn ? bookTitle : displayUrl} + + + {isbn && bookPage && ( + + (p.{bookPage}) + )} - {isbn ? bookTitle : displayUrl} - +
)} -- 2.51.2