Skip to content

Commit 5c7adce

Browse files
committed
📖 Initial documentation
1 parent 7bdd037 commit 5c7adce

File tree

7 files changed

+240
-2
lines changed

7 files changed

+240
-2
lines changed

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
node_modules
22
dist
3+
docs/_build
4+
docs/schema

‎CONTRIBUTING.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pnpm install
1717

1818
The OXA schema is defined as individual YAML files in the `schema/` directory. YAML was chosen for schema definitions because it is more human-readable and easier to edit than JSON.
1919

20-
The `schema/schema.json` file is **auto-generated** from the YAML sources and should not be edited directly. Any schema changes should be made to the `.yaml` files, then regenerated:
20+
The `schema/schema.json` file is **autogenerated** from the YAML sources and should not be edited directly. Any schema changes should be made to the `.yaml` files, then regenerated:
2121

2222
```bash
2323
pnpm codegen json
@@ -72,3 +72,7 @@ pnpm codegen docs # Generate MyST documentation
7272
# Build all packages
7373
pnpm build
7474
```
75+
76+
## Documentation
77+
78+
The documentation uses [MyST](https://mystmd.org) and requires the automated codegen tool to run. To start the documentation use `npm install -g mystmd` and run `myst start` in the `docs/` folder. The online documentation is hosted using Curvenote under https://oxa.dev

‎docs/index.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: OXA Schema
3+
---
4+
5+
> **A foundation for interoperable, structured scientific content.**
6+
> OXA defines open, extensible JSON schemas that describe modular and composable scientific documents — bridging the gap between authoring systems like **Stencila**, **MyST**, **Quarto** and the scientific publishing ecosystem which uses tools like **JATS**.
7+
8+
## Overview
9+
10+
The **Open Exchange Architecture (OXA)** is a specification for representing scientific documents and their components as structured JSON objects.
11+
It’s designed to enable **exchange, interoperability, and long-term preservation** of scientific knowledge, while remaining compatible with modern web and data standards.
12+
13+
OXA provides schemas and examples for representing:
14+
15+
- Executable and interactive research components
16+
- Text, math, figures, code, and metadata
17+
- Authors, affiliations, and licenses
18+
- Hierarchical structures like sections and paragraphs
19+
- Inline formatting (strong, emphasis, quote, etc.)
20+
21+
The format is inspired by **[unified.js](https://unifiedjs.com)** and **Pandoc AST**, following a **typed node model** with `children` arrays that form a tree.

‎docs/myst.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# See docs at: https://mystmd.org/guide/frontmatter
2+
version: 1
3+
project:
4+
id: oxa-docs
5+
title: Open Exchange Architecture
6+
description: A specification for representing scientific documents and their components as structured objects.
7+
license: CC-BY-4.0
8+
github: https://github.com/oxa-dev/oxa
9+
abbreviations:
10+
OXA: Open Exchange Architecture
11+
AST: Abstract Syntax Tree
12+
JATS: Journal Article Tag Suite
13+
JSON: JavaScript Object Notation
14+
toc:
15+
- file: index.md
16+
- title: Schema Reference
17+
children:
18+
- pattern: schema/*.md
19+
site:
20+
template: book-theme

‎packages/oxa-types-ts/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"license": "MIT",
2626
"repository": {
2727
"type": "git",
28-
"url": "https://github.com/oxa-dev/oxa.git",
28+
"url": "git+https://github.com/oxa-dev/oxa.git",
2929
"directory": "packages/oxa-types-ts"
3030
}
3131
}

‎scripts/codegen.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { program } from "commander";
99
import { generateJson } from "./lib/generate-json.js";
1010
import { generateTs } from "./lib/generate-ts.js";
11+
import { generateDocs } from "./lib/generate-docs.js";
1112
import { validateSchemas } from "./lib/validate.js";
1213

1314
interface Generator {
@@ -19,6 +20,7 @@ interface Generator {
1920
const generators: Generator[] = [
2021
{ name: "json", label: "JSON Schema", fn: generateJson },
2122
{ name: "ts", label: "TypeScript types", fn: generateTs },
23+
{ name: "docs", label: "Schema documentation", fn: generateDocs },
2224
];
2325

2426
async function validate(): Promise<void> {
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Generate Markdown documentation files from the OXA JSON Schema.
3+
*
4+
* Creates individual documentation files for each schema type in the docs/schema/
5+
* directory, formatted for use in documentation sites.
6+
*/
7+
8+
import { mkdirSync, rmSync, writeFileSync } from "fs";
9+
import { join } from "path";
10+
11+
import { loadMergedSchema } from "./schema.js";
12+
13+
const OUTPUT_DIR = join(import.meta.dirname, "../../docs/schema");
14+
15+
interface SchemaProperty {
16+
type?: string;
17+
const?: string;
18+
description?: string;
19+
items?: { $ref?: string; type?: string };
20+
$ref?: string;
21+
minimum?: number;
22+
maximum?: number;
23+
additionalProperties?: boolean;
24+
}
25+
26+
interface SchemaDefinition {
27+
title: string;
28+
description?: string;
29+
type?: string;
30+
anyOf?: Array<{ $ref: string }>;
31+
properties?: Record<string, SchemaProperty>;
32+
required?: string[];
33+
}
34+
35+
export async function generateDocs(): Promise<void> {
36+
// Delete and recreate output directory
37+
try {
38+
rmSync(OUTPUT_DIR, { recursive: true, force: true });
39+
} catch (error) {
40+
// Directory might not exist, which is fine
41+
}
42+
mkdirSync(OUTPUT_DIR, { recursive: true });
43+
44+
const schema = loadMergedSchema();
45+
const definitions = schema.definitions as Record<string, SchemaDefinition>;
46+
47+
// Generate documentation for object types (non-union types)
48+
for (const [name, def] of Object.entries(definitions)) {
49+
if (!def.anyOf && def.type === "object") {
50+
const content = generateDocContent(name, def);
51+
const filePath = join(OUTPUT_DIR, `${name.toLowerCase()}.md`);
52+
writeFileSync(filePath, content);
53+
console.log(`Generated ${filePath}`);
54+
}
55+
}
56+
57+
// Generate documentation for union types
58+
for (const [name, def] of Object.entries(definitions)) {
59+
if (def.anyOf) {
60+
const content = generateUnionDocContent(name, def);
61+
const filePath = join(OUTPUT_DIR, `${name.toLowerCase()}.md`);
62+
writeFileSync(filePath, content);
63+
console.log(`Generated ${filePath}`);
64+
}
65+
}
66+
}
67+
68+
function generateDocContent(name: string, def: SchemaDefinition): string {
69+
const lines: string[] = [];
70+
71+
// Frontmatter
72+
lines.push(`(oxa:${name.toLowerCase()})=`);
73+
lines.push("");
74+
75+
// Heading
76+
lines.push(`## ${name}`);
77+
lines.push("");
78+
lines.push("");
79+
80+
// Description
81+
if (def.description) {
82+
lines.push(def.description);
83+
lines.push("");
84+
lines.push("");
85+
}
86+
87+
const required = new Set(def.required || []);
88+
89+
// Properties
90+
for (const [propName, prop] of Object.entries(def.properties || {})) {
91+
// Property header
92+
if (prop.const) {
93+
// Const types use italic _string_, with const value in parentheses
94+
lines.push(`__${propName}__: _string_, ("${prop.const}")`);
95+
} else if (prop.type === "array" && prop.items) {
96+
const arrayType = getArrayItemType(prop.items);
97+
lines.push(`__${propName}__: __array__ ("${arrayType}")`);
98+
} else {
99+
const propType = getPropertyType(prop);
100+
lines.push(`__${propName}__: __${propType}__`);
101+
}
102+
lines.push("");
103+
104+
// Property description
105+
if (prop.description) {
106+
lines.push(`: ${prop.description}`);
107+
}
108+
109+
// Reference hint for ref types (arrays with ref items get the hint)
110+
if (prop.items?.$ref) {
111+
const refName = prop.items.$ref.replace("#/definitions/", "");
112+
lines.push(`: See @oxa:${refName.toLowerCase()}`);
113+
}
114+
115+
lines.push("");
116+
}
117+
118+
return lines.join("\n");
119+
}
120+
121+
function getPropertyType(prop: SchemaProperty): string {
122+
if (prop.$ref) {
123+
const refName = prop.$ref.replace("#/definitions/", "");
124+
return refName;
125+
}
126+
127+
switch (prop.type) {
128+
case "string":
129+
return "string";
130+
case "integer":
131+
case "number":
132+
return "number";
133+
case "boolean":
134+
return "boolean";
135+
case "array":
136+
return "array";
137+
case "object":
138+
return "object";
139+
default:
140+
return "unknown";
141+
}
142+
}
143+
144+
function generateUnionDocContent(name: string, def: SchemaDefinition): string {
145+
const lines: string[] = [];
146+
147+
// Frontmatter
148+
lines.push(`(oxa:${name.toLowerCase()})=`);
149+
lines.push("");
150+
151+
// Heading
152+
lines.push(`## ${name}`);
153+
lines.push("");
154+
lines.push("");
155+
156+
// Description
157+
if (def.description) {
158+
lines.push(def.description);
159+
lines.push("");
160+
lines.push("");
161+
}
162+
163+
// List union members
164+
const members =
165+
def.anyOf?.map((item) => {
166+
const ref = item.$ref;
167+
const typeName = ref?.replace("#/definitions/", "") || "unknown";
168+
return typeName;
169+
}) || [];
170+
171+
if (members.length > 0) {
172+
lines.push(
173+
`Union of: ${members.map((m) => `@oxa:${m.toLowerCase()}`).join(", ")}`,
174+
);
175+
lines.push("");
176+
}
177+
178+
return lines.join("\n");
179+
}
180+
181+
function getArrayItemType(items: { $ref?: string; type?: string }): string {
182+
if (items.$ref) {
183+
return items.$ref.replace("#/definitions/", "");
184+
}
185+
if (items.type) {
186+
return items.type;
187+
}
188+
return "unknown";
189+
}

0 commit comments

Comments
 (0)