Skip to content

Commit fb4485d

Browse files
authored
Merge branch 'main' into oxa-types-py
2 parents 36edf83 + df75e1b commit fb4485d

File tree

8 files changed

+247
-2
lines changed

8 files changed

+247
-2
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ __pycache__
88
.venv
99
.mypy_cache
1010
*.egg-info
11+
12+
# Documentation
13+
docs/_build
14+
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

eslint.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import { defineConfig } from "eslint/config";
44
import tseslint from "typescript-eslint";
55

66
export default defineConfig(
7+
{
8+
ignores: [
9+
"**/node_modules/**",
10+
"**/dist/**",
11+
"**/docs/_build/**",
12+
"**/.git/**",
13+
"**/pnpm-lock.yaml",
14+
],
15+
},
716
eslint.configs.recommended,
817
tseslint.configs.recommended,
918
);

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
@@ -9,6 +9,7 @@ import { program } from "commander";
99
import { generateJson } from "./lib/generate-json.js";
1010
import { generatePy } from "./lib/generate-py.js";
1111
import { generateTs } from "./lib/generate-ts.js";
12+
import { generateDocs } from "./lib/generate-docs.js";
1213
import { validateSchemas } from "./lib/validate.js";
1314

1415
interface Generator {
@@ -21,6 +22,7 @@ const generators: Generator[] = [
2122
{ name: "json", label: "JSON Schema", fn: generateJson },
2223
{ name: "py", label: "Python Pydantic models", fn: generatePy },
2324
{ name: "ts", label: "TypeScript types", fn: generateTs },
25+
{ name: "docs", label: "Schema documentation", fn: generateDocs },
2426
];
2527

2628
async function validate(): Promise<void> {

scripts/lib/generate-docs.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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, existsSync } 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+
if (existsSync(OUTPUT_DIR)) {
38+
rmSync(OUTPUT_DIR, { recursive: true, force: true });
39+
}
40+
mkdirSync(OUTPUT_DIR, { recursive: true });
41+
42+
const schema = loadMergedSchema();
43+
const definitions = schema.definitions as Record<string, SchemaDefinition>;
44+
45+
// Generate documentation for object types (non-union types)
46+
for (const [name, def] of Object.entries(definitions)) {
47+
if (!def.anyOf && def.type === "object") {
48+
const content = generateDocContent(name, def);
49+
const filePath = join(OUTPUT_DIR, `${name.toLowerCase()}.md`);
50+
writeFileSync(filePath, content);
51+
console.log(`Generated ${filePath}`);
52+
}
53+
}
54+
55+
// Generate documentation for union types
56+
for (const [name, def] of Object.entries(definitions)) {
57+
if (def.anyOf) {
58+
const content = generateUnionDocContent(name, def);
59+
const filePath = join(OUTPUT_DIR, `${name.toLowerCase()}.md`);
60+
writeFileSync(filePath, content);
61+
console.log(`Generated ${filePath}`);
62+
}
63+
}
64+
}
65+
66+
function generateDocContent(name: string, def: SchemaDefinition): string {
67+
const lines: string[] = [];
68+
69+
// Frontmatter
70+
lines.push(`(oxa:${name.toLowerCase()})=`);
71+
lines.push("");
72+
73+
// Heading
74+
lines.push(`## ${name}`);
75+
lines.push("");
76+
lines.push("");
77+
78+
// Description
79+
if (def.description) {
80+
lines.push(def.description);
81+
lines.push("");
82+
lines.push("");
83+
}
84+
85+
// Properties
86+
for (const [propName, prop] of Object.entries(def.properties || {})) {
87+
// Property header
88+
if (prop.const) {
89+
// Const types use italic _string_, with const value in parentheses
90+
lines.push(`__${propName}__: _string_, ("${prop.const}")`);
91+
} else if (prop.type === "array" && prop.items) {
92+
const arrayType = getArrayItemType(prop.items);
93+
lines.push(`__${propName}__: __array__ ("${arrayType}")`);
94+
} else {
95+
const propType = getPropertyType(prop);
96+
lines.push(`__${propName}__: __${propType}__`);
97+
}
98+
lines.push("");
99+
100+
// Property description
101+
if (prop.description) {
102+
lines.push(`: ${prop.description}`);
103+
}
104+
105+
// Reference hint for ref types (arrays with ref items get the hint)
106+
if (prop.items?.$ref) {
107+
const refName = prop.items.$ref.replace("#/definitions/", "");
108+
lines.push(`: See @oxa:${refName.toLowerCase()}`);
109+
}
110+
111+
lines.push("");
112+
}
113+
114+
return lines.join("\n");
115+
}
116+
117+
function getPropertyType(prop: SchemaProperty): string {
118+
if (prop.$ref) {
119+
const refName = prop.$ref.replace("#/definitions/", "");
120+
return refName;
121+
}
122+
123+
switch (prop.type) {
124+
case "string":
125+
return "string";
126+
case "integer":
127+
case "number":
128+
return "number";
129+
case "boolean":
130+
return "boolean";
131+
case "array":
132+
return "array";
133+
case "object":
134+
return "object";
135+
default:
136+
return "unknown";
137+
}
138+
}
139+
140+
function generateUnionDocContent(name: string, def: SchemaDefinition): string {
141+
const lines: string[] = [];
142+
143+
// Frontmatter
144+
lines.push(`(oxa:${name.toLowerCase()})=`);
145+
lines.push("");
146+
147+
// Heading
148+
lines.push(`## ${name}`);
149+
lines.push("");
150+
lines.push("");
151+
152+
// Description
153+
if (def.description) {
154+
lines.push(def.description);
155+
lines.push("");
156+
lines.push("");
157+
}
158+
159+
// List union members
160+
const members =
161+
def.anyOf?.map((item) => {
162+
const ref = item.$ref;
163+
const typeName = ref?.replace("#/definitions/", "") || "unknown";
164+
return typeName;
165+
}) || [];
166+
167+
if (members.length > 0) {
168+
lines.push(
169+
`Union of: ${members.map((m) => `@oxa:${m.toLowerCase()}`).join(", ")}`,
170+
);
171+
lines.push("");
172+
}
173+
174+
return lines.join("\n");
175+
}
176+
177+
function getArrayItemType(items: { $ref?: string; type?: string }): string {
178+
if (items.$ref) {
179+
return items.$ref.replace("#/definitions/", "");
180+
}
181+
if (items.type) {
182+
return items.type;
183+
}
184+
return "unknown";
185+
}

0 commit comments

Comments
 (0)