···11+/**
22+ * GENERATED CODE - DO NOT MODIFY
33+ */
44+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55+import { CID } from 'multiformats/cid'
66+import { validate as _validate } from '../../../lexicons'
77+import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
88+import type * as PubLeafletPagesLinearDocument from './pages/linearDocument'
99+import type * as PubLeafletPagesCanvas from './pages/canvas'
1010+1111+const is$typed = _is$typed,
1212+ validate = _validate
1313+const id = 'pub.leaflet.content'
1414+1515+/** Content format for leaflet documents */
1616+export interface Main {
1717+ $type?: 'pub.leaflet.content'
1818+ pages: (
1919+ | $Typed<PubLeafletPagesLinearDocument.Main>
2020+ | $Typed<PubLeafletPagesCanvas.Main>
2121+ | { $type: string }
2222+ )[]
2323+}
2424+2525+const hashMain = 'main'
2626+2727+export function isMain<V>(v: V) {
2828+ return is$typed(v, id, hashMain)
2929+}
3030+3131+export function validateMain<V>(v: V) {
3232+ return validate<Main & V>(v, id, hashMain)
3333+}
+41
lexicons/api/types/site/standard/document.ts
···11+/**
22+ * GENERATED CODE - DO NOT MODIFY
33+ */
44+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55+import { CID } from 'multiformats/cid'
66+import { validate as _validate } from '../../../lexicons'
77+import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
88+import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef'
99+import type * as PubLeafletContent from '../../pub/leaflet/content'
1010+1111+const is$typed = _is$typed,
1212+ validate = _validate
1313+const id = 'site.standard.document'
1414+1515+export interface Record {
1616+ $type: 'site.standard.document'
1717+ bskyPostRef?: ComAtprotoRepoStrongRef.Main
1818+ content?: $Typed<PubLeafletContent.Main> | { $type: string }
1919+ coverImage?: BlobRef
2020+ description?: string
2121+ /** combine with the publication url or the document site to construct a full url to the document */
2222+ path?: string
2323+ publishedAt: string
2424+ /** URI to the site or publication this document belongs to (https or at-uri) */
2525+ site: string
2626+ tags?: string[]
2727+ textContent?: string
2828+ title: string
2929+ updatedAt?: string
3030+ [k: string]: unknown
3131+}
3232+3333+const hashRecord = 'main'
3434+3535+export function isRecord<V>(v: V) {
3636+ return is$typed(v, id, hashRecord)
3737+}
3838+3939+export function validateRecord<V>(v: V) {
4040+ return validate<Record & V>(v, id, hashRecord, true)
4141+}
···11+import { LexiconDoc } from "@atproto/lexicon";
22+import { PubLeafletPagesLinearDocument } from "./pages/LinearDocument";
33+import { PubLeafletPagesCanvasDocument } from "./pages";
44+55+export const PubLeafletContent: LexiconDoc = {
66+ lexicon: 1,
77+ id: "pub.leaflet.content",
88+ revision: 1,
99+ description: "A lexicon for long form rich media documents",
1010+ defs: {
1111+ main: {
1212+ type: "object",
1313+ description: "Content format for leaflet documents",
1414+ required: ["pages"],
1515+ properties: {
1616+ pages: {
1717+ type: "array",
1818+ items: {
1919+ type: "union",
2020+ refs: [
2121+ PubLeafletPagesLinearDocument.id,
2222+ PubLeafletPagesCanvasDocument.id,
2323+ ],
2424+ },
2525+ },
2626+ },
2727+ },
2828+ },
2929+};
+262
lexicons/src/normalize.ts
···11+/**
22+ * Normalization utilities for converting between pub.leaflet and site.standard lexicon formats.
33+ *
44+ * The standard format (site.standard.*) is used as the canonical representation for
55+ * reading data from the database, while both formats are accepted for storage.
66+ */
77+88+import type * as PubLeafletDocument from "../api/types/pub/leaflet/document";
99+import type * as PubLeafletPublication from "../api/types/pub/leaflet/publication";
1010+import type * as PubLeafletContent from "../api/types/pub/leaflet/content";
1111+import type * as SiteStandardDocument from "../api/types/site/standard/document";
1212+import type * as SiteStandardPublication from "../api/types/site/standard/publication";
1313+import type * as SiteStandardThemeBasic from "../api/types/site/standard/theme/basic";
1414+import type * as PubLeafletThemeColor from "../api/types/pub/leaflet/theme/color";
1515+import type { $Typed } from "../api/util";
1616+1717+// Normalized document type - uses the generated site.standard.document type
1818+// with an additional optional theme field for backwards compatibility
1919+export type NormalizedDocument = SiteStandardDocument.Record & {
2020+ // Keep the original theme for components that need leaflet-specific styling
2121+ theme?: PubLeafletPublication.Theme;
2222+};
2323+2424+// Normalized publication type - uses the generated site.standard.publication type
2525+export type NormalizedPublication = SiteStandardPublication.Record;
2626+2727+/**
2828+ * Checks if the record is a pub.leaflet.document
2929+ */
3030+export function isLeafletDocument(
3131+ record: unknown
3232+): record is PubLeafletDocument.Record {
3333+ if (!record || typeof record !== "object") return false;
3434+ const r = record as Record<string, unknown>;
3535+ return (
3636+ r.$type === "pub.leaflet.document" ||
3737+ // Legacy records without $type but with pages array
3838+ (Array.isArray(r.pages) && typeof r.author === "string")
3939+ );
4040+}
4141+4242+/**
4343+ * Checks if the record is a site.standard.document
4444+ */
4545+export function isStandardDocument(
4646+ record: unknown
4747+): record is SiteStandardDocument.Record {
4848+ if (!record || typeof record !== "object") return false;
4949+ const r = record as Record<string, unknown>;
5050+ return r.$type === "site.standard.document";
5151+}
5252+5353+/**
5454+ * Checks if the record is a pub.leaflet.publication
5555+ */
5656+export function isLeafletPublication(
5757+ record: unknown
5858+): record is PubLeafletPublication.Record {
5959+ if (!record || typeof record !== "object") return false;
6060+ const r = record as Record<string, unknown>;
6161+ return (
6262+ r.$type === "pub.leaflet.publication" ||
6363+ // Legacy records without $type but with name and no url
6464+ (typeof r.name === "string" && !("url" in r))
6565+ );
6666+}
6767+6868+/**
6969+ * Checks if the record is a site.standard.publication
7070+ */
7171+export function isStandardPublication(
7272+ record: unknown
7373+): record is SiteStandardPublication.Record {
7474+ if (!record || typeof record !== "object") return false;
7575+ const r = record as Record<string, unknown>;
7676+ return r.$type === "site.standard.publication";
7777+}
7878+7979+/**
8080+ * Extracts RGB values from a color union type
8181+ */
8282+function extractRgb(
8383+ color:
8484+ | $Typed<PubLeafletThemeColor.Rgba>
8585+ | $Typed<PubLeafletThemeColor.Rgb>
8686+ | { $type: string }
8787+ | undefined
8888+): { r: number; g: number; b: number } | undefined {
8989+ if (!color || typeof color !== "object") return undefined;
9090+ const c = color as Record<string, unknown>;
9191+ if (
9292+ typeof c.r === "number" &&
9393+ typeof c.g === "number" &&
9494+ typeof c.b === "number"
9595+ ) {
9696+ return { r: c.r, g: c.g, b: c.b };
9797+ }
9898+ return undefined;
9999+}
100100+101101+/**
102102+ * Converts a pub.leaflet theme to a site.standard.theme.basic format
103103+ */
104104+export function leafletThemeToBasicTheme(
105105+ theme: PubLeafletPublication.Theme | undefined
106106+): SiteStandardThemeBasic.Main | undefined {
107107+ if (!theme) return undefined;
108108+109109+ const background = extractRgb(theme.backgroundColor);
110110+ const accent = extractRgb(theme.accentBackground) || extractRgb(theme.primary);
111111+ const accentForeground = extractRgb(theme.accentText);
112112+113113+ // If we don't have the required colors, return undefined
114114+ if (!background || !accent) return undefined;
115115+116116+ // Default foreground to dark if not specified
117117+ const foreground = { r: 0, g: 0, b: 0 };
118118+119119+ // Default accent foreground to white if not specified
120120+ const finalAccentForeground = accentForeground || { r: 255, g: 255, b: 255 };
121121+122122+ return {
123123+ $type: "site.standard.theme.basic",
124124+ background: { $type: "site.standard.theme.color#rgb", ...background },
125125+ foreground: { $type: "site.standard.theme.color#rgb", ...foreground },
126126+ accent: { $type: "site.standard.theme.color#rgb", ...accent },
127127+ accentForeground: {
128128+ $type: "site.standard.theme.color#rgb",
129129+ ...finalAccentForeground,
130130+ },
131131+ };
132132+}
133133+134134+/**
135135+ * Normalizes a document record from either format to the standard format.
136136+ *
137137+ * @param record - The document record from the database (either pub.leaflet or site.standard)
138138+ * @returns A normalized document in site.standard format, or null if invalid/unrecognized
139139+ */
140140+export function normalizeDocument(record: unknown): NormalizedDocument | null {
141141+ if (!record || typeof record !== "object") return null;
142142+143143+ // Pass through site.standard records directly
144144+ if (isStandardDocument(record)) {
145145+ return record as NormalizedDocument;
146146+ }
147147+148148+ if (isLeafletDocument(record)) {
149149+ // Convert from pub.leaflet to site.standard
150150+ const site = record.publication;
151151+ const publishedAt = record.publishedAt;
152152+153153+ if (!site || !publishedAt) {
154154+ return null;
155155+ }
156156+157157+ // Wrap pages in pub.leaflet.content structure
158158+ const content: $Typed<PubLeafletContent.Main> | undefined = record.pages
159159+ ? {
160160+ $type: "pub.leaflet.content" as const,
161161+ pages: record.pages,
162162+ }
163163+ : undefined;
164164+165165+ return {
166166+ $type: "site.standard.document",
167167+ title: record.title,
168168+ site,
169169+ publishedAt,
170170+ description: record.description,
171171+ tags: record.tags,
172172+ coverImage: record.coverImage,
173173+ bskyPostRef: record.postRef,
174174+ content,
175175+ theme: record.theme,
176176+ };
177177+ }
178178+179179+ return null;
180180+}
181181+182182+/**
183183+ * Normalizes a publication record from either format to the standard format.
184184+ *
185185+ * @param record - The publication record from the database (either pub.leaflet or site.standard)
186186+ * @returns A normalized publication in site.standard format, or null if invalid/unrecognized
187187+ */
188188+export function normalizePublication(
189189+ record: unknown
190190+): NormalizedPublication | null {
191191+ if (!record || typeof record !== "object") return null;
192192+193193+ // Pass through site.standard records directly
194194+ if (isStandardPublication(record)) {
195195+ return record;
196196+ }
197197+198198+ if (isLeafletPublication(record)) {
199199+ // Convert from pub.leaflet to site.standard
200200+ const url = record.base_path ? `https://${record.base_path}` : undefined;
201201+202202+ if (!url) {
203203+ return null;
204204+ }
205205+206206+ const basicTheme = leafletThemeToBasicTheme(record.theme);
207207+208208+ // Convert preferences to site.standard format (strip/replace $type)
209209+ const preferences: SiteStandardPublication.Preferences | undefined =
210210+ record.preferences
211211+ ? {
212212+ showInDiscover: record.preferences.showInDiscover,
213213+ showComments: record.preferences.showComments,
214214+ showMentions: record.preferences.showMentions,
215215+ showPrevNext: record.preferences.showPrevNext,
216216+ }
217217+ : undefined;
218218+219219+ return {
220220+ $type: "site.standard.publication",
221221+ name: record.name,
222222+ url,
223223+ description: record.description,
224224+ icon: record.icon,
225225+ basicTheme,
226226+ theme: record.theme,
227227+ preferences,
228228+ };
229229+ }
230230+231231+ return null;
232232+}
233233+234234+/**
235235+ * Type guard to check if a normalized document has leaflet content
236236+ */
237237+export function hasLeafletContent(
238238+ doc: NormalizedDocument
239239+): doc is NormalizedDocument & {
240240+ content: $Typed<PubLeafletContent.Main>;
241241+} {
242242+ return (
243243+ doc.content !== undefined &&
244244+ (doc.content as { $type?: string }).$type === "pub.leaflet.content"
245245+ );
246246+}
247247+248248+/**
249249+ * Gets the pages array from a normalized document, handling both formats
250250+ */
251251+export function getDocumentPages(
252252+ doc: NormalizedDocument
253253+): PubLeafletContent.Main["pages"] | undefined {
254254+ if (!doc.content) return undefined;
255255+256256+ if (hasLeafletContent(doc)) {
257257+ return doc.content.pages;
258258+ }
259259+260260+ // Unknown content type
261261+ return undefined;
262262+}