Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
TypeScript
at perf/paste
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395/** * Normalization utilities for converting between pub.leaflet and site.standard lexicon formats. * * The standard format (site.standard.*) is used as the canonical representation for * reading data from the database, while both formats are accepted for storage. * * ## Site Field Format * * The `site` field in site.standard.document supports two URI formats: * - AT-URIs (at://did/collection/rkey) - Used when document belongs to an AT Protocol publication * - HTTPS URLs (https://example.com) - Used for standalone documents or external sites * * Both formats are valid and should be handled by consumers. */
import type * as PubLeafletDocument from "../api/types/pub/leaflet/document";import * as PubLeafletPublication from "../api/types/pub/leaflet/publication";import type * as PubLeafletContent from "../api/types/pub/leaflet/content";import type * as SiteStandardDocument from "../api/types/site/standard/document";import type * as SiteStandardPublication from "../api/types/site/standard/publication";import type * as SiteStandardThemeBasic from "../api/types/site/standard/theme/basic";import type * as SiteStandardThemeColor from "../api/types/site/standard/theme/color";import type * as PubLeafletThemeColor from "../api/types/pub/leaflet/theme/color";import type { $Typed } from "../api/util";import { AtUri } from "@atproto/syntax";
// Normalized document type - uses the generated site.standard.document type// with an additional optional theme field for backwards compatibilityexport type NormalizedDocument = SiteStandardDocument.Record & { // Keep the original theme for components that need leaflet-specific styling theme?: PubLeafletPublication.Theme; preferences?: SiteStandardPublication.Preferences;};
// Normalized publication type - uses the generated site.standard.publication type// with the theme narrowed to only the valid pub.leaflet.publication#theme type// (isTheme validates that $type is present, so we use $Typed)// Note: We explicitly list fields rather than using Omit because the generated Record type// has an index signature [k: string]: unknown that interferes with property typingexport type NormalizedPublication = { $type: "site.standard.publication"; name: string; url: string; description?: string; icon?: SiteStandardPublication.Record["icon"]; basicTheme?: SiteStandardThemeBasic.Main; theme?: $Typed<PubLeafletPublication.Theme>; preferences?: SiteStandardPublication.Preferences;};
/** * Checks if the record is a pub.leaflet.document */export function isLeafletDocument( record: unknown,): record is PubLeafletDocument.Record { if (!record || typeof record !== "object") return false; const r = record as Record<string, unknown>; return ( r.$type === "pub.leaflet.document" || // Legacy records without $type but with pages array (Array.isArray(r.pages) && typeof r.author === "string") );}
/** * Checks if the record is a site.standard.document */export function isStandardDocument( record: unknown,): record is SiteStandardDocument.Record { if (!record || typeof record !== "object") return false; const r = record as Record<string, unknown>; return r.$type === "site.standard.document";}
/** * Checks if the record is a pub.leaflet.publication */export function isLeafletPublication( record: unknown,): record is PubLeafletPublication.Record { if (!record || typeof record !== "object") return false; const r = record as Record<string, unknown>; return ( r.$type === "pub.leaflet.publication" || // Legacy records without $type but with name and no url (typeof r.name === "string" && !("url" in r)) );}
/** * Checks if the record is a site.standard.publication */export function isStandardPublication( record: unknown,): record is SiteStandardPublication.Record { if (!record || typeof record !== "object") return false; const r = record as Record<string, unknown>; return r.$type === "site.standard.publication";}
/** * Extracts RGB values from a color union type */function extractRgb( color: | $Typed<PubLeafletThemeColor.Rgba> | $Typed<PubLeafletThemeColor.Rgb> | { $type: string } | undefined,): { r: number; g: number; b: number } | undefined { if (!color || typeof color !== "object") return undefined; const c = color as Record<string, unknown>; if ( typeof c.r === "number" && typeof c.g === "number" && typeof c.b === "number" ) { return { r: c.r, g: c.g, b: c.b }; } return undefined;}
/** * Converts a site.standard.theme.basic into a partial pub.leaflet theme, * tagging colors with the pub.leaflet $type so PublicationThemeProvider can * consume them. Used as a fallback when a publication record carries * basicTheme but no full theme. */export function basicThemeToLeafletTheme( basic: SiteStandardThemeBasic.Main | undefined | null,): $Typed<PubLeafletPublication.Theme> | undefined { if (!basic) return undefined; const toLeafletColor = ( c: $Typed<SiteStandardThemeColor.Rgb> | { $type: string }, ): $Typed<PubLeafletThemeColor.Rgb> | undefined => { const rgb = extractRgb(c); if (!rgb) return undefined; return { $type: "pub.leaflet.theme.color#rgb", ...rgb }; }; const backgroundColor = toLeafletColor(basic.background); const primary = toLeafletColor(basic.foreground); const accentBackground = toLeafletColor(basic.accent); const accentText = toLeafletColor(basic.accentForeground); if (!backgroundColor) return undefined; return { $type: "pub.leaflet.publication#theme", backgroundColor, primary, accentBackground, accentText, showPageBackground: false, };}
/** * Returns the effective pub.leaflet theme for a publication record, preferring * the full theme when present and falling back to basicTheme otherwise. */export function resolvePublicationTheme( record: | { theme?: PubLeafletPublication.Theme | null; basicTheme?: SiteStandardThemeBasic.Main | null; } | null | undefined,): PubLeafletPublication.Theme | undefined { if (!record) return undefined; if (record.theme) return record.theme; if (record.basicTheme) return basicThemeToLeafletTheme(record.basicTheme); return undefined;}
/** * Converts a pub.leaflet theme to a site.standard.theme.basic format */export function leafletThemeToBasicTheme( theme: PubLeafletPublication.Theme | undefined,): SiteStandardThemeBasic.Main | undefined { if (!theme) return undefined;
const background = extractRgb(theme.backgroundColor); const accent = extractRgb(theme.accentBackground) || extractRgb(theme.primary); const accentForeground = extractRgb(theme.accentText);
// If we don't have the required colors, return undefined if (!background || !accent) return undefined;
// Default foreground to dark if not specified const foreground = { r: 0, g: 0, b: 0 };
// Default accent foreground to white if not specified const finalAccentForeground = accentForeground || { r: 255, g: 255, b: 255 };
return { $type: "site.standard.theme.basic", background: { $type: "site.standard.theme.color#rgb", ...background }, foreground: { $type: "site.standard.theme.color#rgb", ...foreground }, accent: { $type: "site.standard.theme.color#rgb", ...accent }, accentForeground: { $type: "site.standard.theme.color#rgb", ...finalAccentForeground, }, };}
/** * Normalizes a document record from either format to the standard format. * * @param record - The document record from the database (either pub.leaflet or site.standard) * @param uri - Optional document URI, used to extract the rkey for the path field when normalizing pub.leaflet records * @returns A normalized document in site.standard format, or null if invalid/unrecognized */export function normalizeDocument( record: unknown, uri?: string,): NormalizedDocument | null { if (!record || typeof record !== "object") return null;
// Pass through site.standard records directly (theme is already in correct format if present) if (isStandardDocument(record)) { const preferences = record.preferences as | SiteStandardPublication.Preferences | undefined; return { ...record, theme: record.theme, preferences, } as NormalizedDocument; }
if (isLeafletDocument(record)) { // Convert from pub.leaflet to site.standard const publishedAt = record.publishedAt;
if (!publishedAt) { return null; }
// For standalone documents (no publication), construct a site URL from the author // This matches the pattern used in publishToPublication.ts for new standalone docs const site = record.publication || `https://leaflet.pub/p/${record.author}`;
// Extract path from URI if available const path = uri ? new AtUri(uri).rkey : undefined;
// Wrap pages in pub.leaflet.content structure const content: $Typed<PubLeafletContent.Main> | undefined = record.pages ? { $type: "pub.leaflet.content" as const, pages: record.pages, } : undefined;
// Extract preferences if present (available after lexicon rebuild) const leafletPrefs = (record as Record<string, unknown>) .preferences as SiteStandardPublication.Preferences | undefined;
return { $type: "site.standard.document", title: record.title, site, path, publishedAt, description: record.description, tags: record.tags, coverImage: record.coverImage, bskyPostRef: record.postRef, content, theme: record.theme, preferences: leafletPrefs ? { ...leafletPrefs, $type: "site.standard.publication#preferences" as const } : undefined, }; }
return null;}
/** * Normalizes a publication record from either format to the standard format. * * @param record - The publication record from the database (either pub.leaflet or site.standard) * @returns A normalized publication in site.standard format, or null if invalid/unrecognized */export function normalizePublication( record: unknown,): NormalizedPublication | null { if (!record || typeof record !== "object") return null;
// Pass through site.standard records directly, but validate the theme if (isStandardPublication(record)) { // Keep the theme if present, adding the $type for legacy records that were // published without it (otherwise non-color theme fields like pageWidth and // fonts would be dropped here while colors survive via basicTheme). let theme: $Typed<PubLeafletPublication.Theme> | undefined; if (record.theme) { theme = PubLeafletPublication.isTheme(record.theme) ? (record.theme as $Typed<PubLeafletPublication.Theme>) : { ...(record.theme as PubLeafletPublication.Theme), $type: "pub.leaflet.publication#theme", }; } return { ...record, theme, }; }
if (isLeafletPublication(record)) { // Convert from pub.leaflet to site.standard const url = record.base_path ? `https://${record.base_path}` : undefined;
if (!url) { return null; }
const basicTheme = leafletThemeToBasicTheme(record.theme);
// Validate theme - only keep if it's a valid pub.leaflet.publication#theme with $type set // For legacy records without $type, add it during normalization let theme: $Typed<PubLeafletPublication.Theme> | undefined; if (record.theme) { if (PubLeafletPublication.isTheme(record.theme)) { theme = record.theme as $Typed<PubLeafletPublication.Theme>; } else { // Legacy theme without $type - add it theme = { ...record.theme, $type: "pub.leaflet.publication#theme", }; } }
// Convert preferences to site.standard format (strip/replace $type) const preferences: SiteStandardPublication.Preferences | undefined = record.preferences ? { showInDiscover: record.preferences.showInDiscover, showComments: record.preferences.showComments, showMentions: record.preferences.showMentions, showPrevNext: record.preferences.showPrevNext, showRecommends: record.preferences.showRecommends, } : undefined;
return { $type: "site.standard.publication", name: record.name, url, description: record.description, icon: record.icon, basicTheme, theme, preferences, }; }
return null;}
/** * Type guard to check if a normalized document has leaflet content */export function hasLeafletContent( doc: NormalizedDocument,): doc is NormalizedDocument & { content: $Typed<PubLeafletContent.Main>;} { return ( doc.content !== undefined && (doc.content as { $type?: string }).$type === "pub.leaflet.content" );}
/** * Gets the pages array from a normalized document, handling both formats */export function getDocumentPages( doc: NormalizedDocument,): PubLeafletContent.Main["pages"] | undefined { if (!doc.content) return undefined;
if (hasLeafletContent(doc)) { return doc.content.pages; }
// Unknown content type return undefined;}