diff --git a/docs/atproto-lexicon.md b/docs/atproto-lexicon.md index 18af741..e932d9d 100644 --- a/docs/atproto-lexicon.md +++ b/docs/atproto-lexicon.md @@ -19,12 +19,12 @@ By defining a lexicon, OXA documents become first-class objects on the AT Protoc ## Lexicon structure -The OXA lexicon is organized into two namespaces: +The OXA lexicon is organized into three namespaces: | File | NSID | Purpose | | -------------------------------- | --------------------------- | --------------------------------------------------------------------------------------- | -| `lexicon/document/document.json` | `pub.oxa.document.document` | The `Document` record type — the root object stored in a PDS | -| `lexicon/document/defs.json` | `pub.oxa.document.defs` | Block-level type definitions (`paragraph`, `heading`, `richText`) and the `block` union | +| `lexicon/document/document.json` | `pub.oxa.document` | The `Document` record type — the root object stored in a PDS | +| `lexicon/blocks/defs.json` | `pub.oxa.blocks.defs` | Block-level type definitions (`paragraph`, `heading`, `richText`) and the `block` union | | `lexicon/richtext/facet.json` | `pub.oxa.richtext.facet` | Facet annotations for inline formatting (`emphasis`, `strong`, `byteSlice`) | A `Document` record contains an array of `children` (blocks). Each block carries a `text` string and an optional `facets` array that annotates ranges of that text with formatting features. @@ -104,7 +104,7 @@ AT Protocol [uses facets instead of a tree](https://www.pfrazee.com/blog/why-fac ```json { - "$type": "pub.oxa.document.defs#paragraph", + "$type": "pub.oxa.blocks.defs#paragraph", "text": "This is bold and italic text.", "facets": [ { @@ -187,16 +187,16 @@ Produces: ```json { - "$type": "pub.oxa.document.document", + "$type": "pub.oxa.document", "children": [ { - "$type": "pub.oxa.document.defs#heading", + "$type": "pub.oxa.blocks.defs#heading", "level": 1, "text": "Hello", "facets": [] }, { - "$type": "pub.oxa.document.defs#paragraph", + "$type": "pub.oxa.blocks.defs#paragraph", "text": "Some emphasized text.", "facets": [ { @@ -248,8 +248,8 @@ The lexicon files are generated from the OXA YAML schema definitions by the code 1. Loads the merged OXA JSON Schema. 2. Classifies each type as inline or block based on the `Inline` and `Block` union definitions. 3. Maps inline types to facet features in `pub.oxa.richtext.facet` (excluding `Text`, which becomes the plain text string). -4. Maps block types to object definitions in `pub.oxa.document.defs`, replacing their inline `children` arrays with `text` + `facets` pairs. -5. Emits the `Document` record type in `pub.oxa.document.document`. +4. Maps block types to object definitions in `pub.oxa.blocks.defs`, replacing their inline `children` arrays with `text` + `facets` pairs. +5. Emits the `Document` record type in `pub.oxa.document`. To regenerate the lexicon after changing the schema: diff --git a/examples/rfc0003.atproto.json b/examples/rfc0003.atproto.json index e62d593..af9867b 100644 --- a/examples/rfc0003.atproto.json +++ b/examples/rfc0003.atproto.json @@ -1,5 +1,5 @@ { - "$type": "pub.oxa.document.document", + "$type": "pub.oxa.document", "title": { "text": "Water Dissociation: H2O → H+ + OH−", "facets": [ @@ -19,13 +19,13 @@ }, "children": [ { - "$type": "pub.oxa.document.defs#heading", + "$type": "pub.oxa.blocks.defs#heading", "text": "Introduction", "facets": [], "level": 1 }, { - "$type": "pub.oxa.document.defs#paragraph", + "$type": "pub.oxa.blocks.defs#paragraph", "text": "Water (H2O) undergoes autoionization, a process in which a water molecule donates a proton to another. The equilibrium constant for this reaction, Kw, is approximately 10−14 at 25 °C.", "facets": [ { @@ -50,15 +50,15 @@ } ] }, - { "$type": "pub.oxa.document.defs#thematicBreak" }, + { "$type": "pub.oxa.blocks.defs#thematicBreak" }, { - "$type": "pub.oxa.document.defs#heading", + "$type": "pub.oxa.blocks.defs#heading", "text": "Computing the Equilibrium", "facets": [], "level": 2 }, { - "$type": "pub.oxa.document.defs#paragraph", + "$type": "pub.oxa.blocks.defs#paragraph", "text": "The following Python snippet computes Kw from ion concentrations:", "facets": [ { @@ -68,12 +68,12 @@ ] }, { - "$type": "pub.oxa.document.defs#code", + "$type": "pub.oxa.blocks.defs#code", "value": "H_plus = 1e-7 # mol/L\nOH_minus = 1e-7 # mol/L\nKw = H_plus * OH_minus\nprint(f\"Kw = {Kw:.2e}\") # Kw = 1.00e-14", "language": "python" }, { - "$type": "pub.oxa.document.defs#paragraph", + "$type": "pub.oxa.blocks.defs#paragraph", "text": "You can run this with python kw.py. The result confirms the well-known value of Kw.", "facets": [ { @@ -90,15 +90,15 @@ } ] }, - { "$type": "pub.oxa.document.defs#thematicBreak" }, + { "$type": "pub.oxa.blocks.defs#thematicBreak" }, { - "$type": "pub.oxa.document.defs#heading", + "$type": "pub.oxa.blocks.defs#heading", "text": "Summary", "facets": [], "level": 2 }, { - "$type": "pub.oxa.document.defs#paragraph", + "$type": "pub.oxa.blocks.defs#paragraph", "text": "This example demonstrates every node type from RFC0003: Heading, Paragraph, Code, ThematicBreak, Text, Emphasis, Strong, Superscript, Subscript, and InlineCode.", "facets": [ { diff --git a/lexicon/document/defs.json b/lexicon/blocks/defs.json similarity index 98% rename from lexicon/document/defs.json rename to lexicon/blocks/defs.json index 904c586..12333d3 100644 --- a/lexicon/document/defs.json +++ b/lexicon/blocks/defs.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "pub.oxa.document.defs", + "id": "pub.oxa.blocks.defs", "defs": { "richText": { "type": "object", diff --git a/lexicon/document/document.json b/lexicon/document.json similarity index 87% rename from lexicon/document/document.json rename to lexicon/document.json index b2bcac3..2a555b8 100644 --- a/lexicon/document/document.json +++ b/lexicon/document.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "pub.oxa.document.document", + "id": "pub.oxa.document", "defs": { "main": { "type": "record", @@ -26,13 +26,13 @@ }, "title": { "type": "ref", - "ref": "pub.oxa.document.defs#richText" + "ref": "pub.oxa.blocks.defs#richText" }, "children": { "type": "array", "items": { "type": "ref", - "ref": "pub.oxa.document.defs#block" + "ref": "pub.oxa.blocks.defs#block" } }, "createdAt": { diff --git a/packages/oxa-core/src/convert.test.ts b/packages/oxa-core/src/convert.test.ts index 131a5cd..f35457a 100644 --- a/packages/oxa-core/src/convert.test.ts +++ b/packages/oxa-core/src/convert.test.ts @@ -26,11 +26,11 @@ const lexiconFiles = { path: resolve(REPO_ROOT, "lexicon/richtext/facet.json"), }, defs: { - id: "pub.oxa.document.defs", - path: resolve(REPO_ROOT, "lexicon/document/defs.json"), + id: "pub.oxa.blocks.defs", + path: resolve(REPO_ROOT, "lexicon/blocks/defs.json"), }, document: { - id: "pub.oxa.document.document", + id: "pub.oxa.document", path: resolve(REPO_ROOT, "lexicon/document/document.json"), }, } as const; @@ -342,9 +342,9 @@ describe("ATProto lexicon structure", () => { expect(main.type).toBe("record"); expect(main.key).toBe("tid"); expect(record.required).toEqual(["children", "createdAt"]); - expect(record.properties.title.ref).toBe("pub.oxa.document.defs#richText"); + expect(record.properties.title.ref).toBe("pub.oxa.blocks.defs#richText"); expect(record.properties.children.items.ref).toBe( - "pub.oxa.document.defs#block", + "pub.oxa.blocks.defs#block", ); expect(record.properties.createdAt).toEqual({ type: "string", @@ -380,7 +380,7 @@ describe("mapBlock", () => { ); await expect(map(block)).resolves.toEqual({ - $type: "pub.oxa.document.defs#paragraph", + $type: "pub.oxa.blocks.defs#paragraph", id: "para-1", classes: ["lead"], data: { align: "left" }, @@ -402,7 +402,7 @@ describe("mapBlock", () => { }); await expect(map(block)).resolves.toEqual({ - $type: "pub.oxa.document.defs#heading", + $type: "pub.oxa.blocks.defs#heading", id: "intro", classes: ["hero"], data: { section: true }, @@ -464,7 +464,7 @@ describe("oxaToAtproto", () => { ); await expect(convertDocument(document, { createdAt })).resolves.toEqual({ - $type: "pub.oxa.document.document", + $type: "pub.oxa.document", title: { text: "Hello, World", facets: [], @@ -475,13 +475,13 @@ describe("oxaToAtproto", () => { }, children: [ { - $type: "pub.oxa.document.defs#heading", + $type: "pub.oxa.blocks.defs#heading", level: 1, text: "Introduction", facets: [], }, { - $type: "pub.oxa.document.defs#paragraph", + $type: "pub.oxa.blocks.defs#paragraph", text: "This is bold and italic text.", facets: [ { @@ -505,7 +505,7 @@ describe("oxaToAtproto", () => { await expect( convertDocument(documentNode([]), { createdAt }), ).resolves.toEqual({ - $type: "pub.oxa.document.document", + $type: "pub.oxa.document", children: [], createdAt, }); @@ -539,11 +539,11 @@ describe("oxaToAtproto", () => { ); expect(converted).toEqual({ - $type: "pub.oxa.document.document", + $type: "pub.oxa.document", metadata, children: [ { - $type: "pub.oxa.document.defs#paragraph", + $type: "pub.oxa.blocks.defs#paragraph", text: "Keep this paragraph", facets: [], }, diff --git a/packages/oxa-core/src/convert.ts b/packages/oxa-core/src/convert.ts index e303807..834cec4 100644 --- a/packages/oxa-core/src/convert.ts +++ b/packages/oxa-core/src/convert.ts @@ -96,23 +96,23 @@ interface RichText { type AtprotoParagraph = RichText & BlockNodeBase & { - $type: "pub.oxa.document.defs#paragraph"; + $type: "pub.oxa.blocks.defs#paragraph"; }; type AtprotoHeading = RichText & BlockNodeBase & { - $type: "pub.oxa.document.defs#heading"; + $type: "pub.oxa.blocks.defs#heading"; level: number; }; type AtprotoCode = BlockNodeBase & { - $type: "pub.oxa.document.defs#code"; + $type: "pub.oxa.blocks.defs#code"; value: string; language?: string; }; type AtprotoThematicBreak = BlockNodeBase & { - $type: "pub.oxa.document.defs#thematicBreak"; + $type: "pub.oxa.blocks.defs#thematicBreak"; }; type AtprotoBlock = @@ -122,7 +122,7 @@ type AtprotoBlock = | AtprotoThematicBreak; type AtprotoDocument = { - $type: "pub.oxa.document.document"; + $type: "pub.oxa.document"; title?: RichText; metadata?: Record; children: AtprotoBlock[]; @@ -177,10 +177,10 @@ export const compatibleFeatures: Record< const formattingPropertyNames = ["id", "classes", "data"] as const; const blockPropertyNames = ["id", "classes", "data"] as const; -const paragraphType = "pub.oxa.document.defs#paragraph" as const; -const headingType = "pub.oxa.document.defs#heading" as const; -const codeType = "pub.oxa.document.defs#code" as const; -const thematicBreakType = "pub.oxa.document.defs#thematicBreak" as const; +const paragraphType = "pub.oxa.blocks.defs#paragraph" as const; +const headingType = "pub.oxa.blocks.defs#heading" as const; +const codeType = "pub.oxa.blocks.defs#code" as const; +const thematicBreakType = "pub.oxa.blocks.defs#thematicBreak" as const; const encoder = new TextEncoder(); @@ -414,7 +414,7 @@ export function oxaToAtproto( options: OxaToAtprotoOptions = {}, ): AtprotoDocument { return { - $type: "pub.oxa.document.document", + $type: "pub.oxa.document", ...getOptionalDocumentFields(session, document), children: mapKnownBlocks(session, document.children), createdAt: options.createdAt ?? new Date().toISOString(), diff --git a/packages/oxa/src/cli.test.ts b/packages/oxa/src/cli.test.ts index d440f08..f9c722a 100644 --- a/packages/oxa/src/cli.test.ts +++ b/packages/oxa/src/cli.test.ts @@ -77,7 +77,7 @@ describe("oxa cli", () => { describe("convert", () => { const createdAt = "2026-03-22T00:00:00.000Z"; const expectedConverted = { - $type: "pub.oxa.document.document", + $type: "pub.oxa.document", title: { text: "CLI Example", facets: [], @@ -85,7 +85,7 @@ describe("oxa cli", () => { metadata: { license: "CC-BY-4.0" }, children: [ { - $type: "pub.oxa.document.defs#paragraph", + $type: "pub.oxa.blocks.defs#paragraph", text: "Hello from CLI", facets: [], }, diff --git a/scripts/lib/generate-lexicon.ts b/scripts/lib/generate-lexicon.ts index 0e255d5..8573f40 100644 --- a/scripts/lib/generate-lexicon.ts +++ b/scripts/lib/generate-lexicon.ts @@ -10,8 +10,8 @@ * model (a plain text string + facets with byte-slice annotations). * * Output files: - * lexicon/richtext/facet.json — facet definition + inline feature types - * lexicon/document/defs.json — block-level type defs and rich text helper + * lexicon/richtext/facet.json — facet definition + inline feature types + * lexicon/blocks/defs.json — block-level type defs and rich text helper * lexicon/document/document.json — the Document record type */ @@ -59,16 +59,13 @@ export async function generateLexicon(): Promise { // Generate the three lexicon files const facetFile = generateFacetLexicon(definitions, inlineMembers); - const defsFile = generateDefsLexicon(definitions, blockMembers); + const blocksDefsFile = generateBlocksDefsLexicon(definitions, blockMembers); const documentFile = generateDocumentLexicon(definitions); // Write files writeLexiconFile(join(LEXICON_DIR, "richtext", "facet.json"), facetFile); - writeLexiconFile(join(LEXICON_DIR, "document", "defs.json"), defsFile); - writeLexiconFile( - join(LEXICON_DIR, "document", "document.json"), - documentFile, - ); + writeLexiconFile(join(LEXICON_DIR, "blocks", "defs.json"), blocksDefsFile); + writeLexiconFile(join(LEXICON_DIR, "document.json"), documentFile); console.log(`Generated lexicon files in ${LEXICON_DIR}`); } @@ -163,14 +160,14 @@ function generateFacetLexicon( } /** - * Generate pub.oxa.document.defs lexicon. + * Generate pub.oxa.blocks.defs lexicon. * * Contains: * - richText: reusable object with text + facets (like Bluesky's pattern) * - One def per block type, with children: Inline[] flattened to richText * - block: union of all block types */ -function generateDefsLexicon( +function generateBlocksDefsLexicon( definitions: Record, blockMembers: string[], ): LexiconFile { @@ -254,13 +251,13 @@ function generateDefsLexicon( return { lexicon: 1, - id: "pub.oxa.document.defs", + id: "pub.oxa.blocks.defs", defs, }; } /** - * Generate pub.oxa.document.document lexicon. + * Generate pub.oxa.document lexicon. * * The Document type becomes a record (matching Bluesky's app.bsky.feed.post pattern). */ @@ -284,7 +281,7 @@ function generateDocumentLexicon( if (propName === "title" && isInlineArray(prop)) { recordProperties["title"] = { type: "ref", - ref: "pub.oxa.document.defs#richText", + ref: "pub.oxa.blocks.defs#richText", }; continue; } @@ -295,7 +292,7 @@ function generateDocumentLexicon( type: "array", items: { type: "ref", - ref: "pub.oxa.document.defs#block", + ref: "pub.oxa.blocks.defs#block", }, }; if (schemaRequired.has(propName)) { @@ -331,7 +328,7 @@ function generateDocumentLexicon( return { lexicon: 1, - id: "pub.oxa.document.document", + id: "pub.oxa.document", defs: { main: { type: "record", @@ -451,8 +448,8 @@ function extractNonStructuralProperties( * This is a placeholder for when more types are added to the schema. */ function resolveRefToLexicon(typeName: string): string { - // For now, types map to document defs - return `pub.oxa.document.defs#${toCamelCase(typeName)}`; + // For now, types map to block defs + return `pub.oxa.blocks.defs#${toCamelCase(typeName)}`; } function toCamelCase(s: string): string {