Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### Added

- Shipped every `.opf.json` deck under `examples/` inside the npm package and exposed them via `@openpresentation/opf/examples` (`examples`, `galleries`, `exampleCategories`, `getExample`, `getGallery`, `getExamplesByGallery`, `getExamplesByCategory`). Each example deck is validated against the presentation schema at build time.
- Shipped the top-level `docs/*.md` reference pages inside the npm package and exposed them via `@openpresentation/opf/docs` (`docs`, `getDoc`). Subdirectories like `docs/migrations` and `docs/plans` are intentionally excluded.
- Shipped the upstream `README.md` markdown at the pinned release version via `@openpresentation/opf/repo-readme` so consumer sites can mirror it without a network fetch.

## 0.2.2

### Changed
Expand Down
56 changes: 56 additions & 0 deletions packages/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,62 @@ export function LayoutThumbnail({ slug }: { slug: string }) {
Raw HTML source-of-truth lives under `spec/previews/layouts/<slug>.html` and is
also addressable via `@openpresentation/opf/spec/previews/layouts/<slug>.html`.

### Example decks

`@openpresentation/opf/examples` ships every `.opf.json` file from `examples/`
in the upstream repo, already parsed and validated against the presentation
schema at build time.

```ts
import {
examples,
galleries,
getExample,
getExamplesByGallery,
} from "@openpresentation/opf/examples";

const compliance = getExample("compliance-readiness-review");
const businessDecks = getExamplesByGallery("business-functions");

console.log(`${examples.length} example decks across ${galleries.length} galleries`);
```

Each `ExampleRecord` includes the parsed `Presentation` object plus its
repo-relative path, top-level category (`gallery`, `technical`, …), and
gallery slug when applicable.

### Documentation pages

`@openpresentation/opf/docs` ships the top-level `docs/*.md` reference pages
(`schema-reference`, `catalog-schema-reference`, `content-payloads`,
`content-item-design-overrides`, `examples`). Subdirectories like
`docs/migrations` and `docs/plans` are intentionally excluded — those move too
quickly to ship inside a pinned npm release.

```ts
import { docs, getDoc } from "@openpresentation/opf/docs";

for (const doc of docs) {
console.log(`${doc.title} — ${doc.file}`);
}

const schemaRef = getDoc("schema-reference");
console.log(schemaRef?.markdown.slice(0, 200));
```

### Upstream README

`@openpresentation/opf/repo-readme` exposes the raw markdown of the upstream
`OpenPresentation/opf` README.md at the version pinned by this release. Use it
when you want to mirror the canonical README inside another site or app
without doing a network fetch.

```ts
import { repoReadme } from "@openpresentation/opf/repo-readme";

console.log(repoReadme.split("\n").slice(0, 3).join("\n"));
```

Validate catalog records locally:

```ts
Expand Down
14 changes: 13 additions & 1 deletion packages/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@
"types": "./dist/previews.d.ts",
"import": "./dist/previews.js"
},
"./examples": {
"types": "./dist/examples.d.ts",
"import": "./dist/examples.js"
},
"./docs": {
"types": "./dist/docs.d.ts",
"import": "./dist/docs.js"
},
"./repo-readme": {
"types": "./dist/repo-readme.d.ts",
"import": "./dist/repo-readme.js"
},
"./spec/*": "./dist/spec/*",
"./package.json": "./package.json"
},
Expand All @@ -72,7 +84,7 @@
},
"scripts": {
"clean": "rm -rf dist src/generated",
"generate": "node scripts/generate.mjs && node scripts/generate-previews.mjs",
"generate": "node scripts/generate.mjs && node scripts/generate-previews.mjs && node scripts/generate-content.mjs",
"build": "pnpm run generate && tsup && node scripts/copy-spec.mjs",
"typecheck": "pnpm run generate && tsc --noEmit",
"test": "pnpm run build && node test/smoke.mjs",
Expand Down
202 changes: 202 additions & 0 deletions packages/javascript/scripts/generate-content.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Builds src/generated/{examples,docs,repo-readme}.ts by inlining the example
// .opf.json decks, the top-level docs/*.md files, and the repo README.md.
//
// Source-of-truth lives at the repo root:
// - examples/**/*.opf.json
// - docs/*.md (top-level only; subdirectories like docs/migrations and
// docs/plans are intentionally excluded for now — they
// change too often to ship inside a pinned npm release)
// - README.md
//
// Run via `pnpm generate` (wired in package.json after the schema/catalog gen).

import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(__dirname, "..");
const repoRoot = path.resolve(packageRoot, "../..");
const examplesRoot = path.join(repoRoot, "examples");
const docsRoot = path.join(repoRoot, "docs");
const readmePath = path.join(repoRoot, "README.md");
const generatedRoot = path.join(packageRoot, "src/generated");

await fs.mkdir(generatedRoot, { recursive: true });

function generatedHeader(source) {
return `// Generated by scripts/generate-content.mjs from ${source}. Do not edit by hand.\n\n`;
}

function posix(relPath) {
return relPath.split(path.sep).join(path.posix.sep);
}

async function collectFiles(root, predicate) {
const out = [];
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walk(abs);
} else if (entry.isFile()) {
const rel = posix(path.relative(root, abs));
if (predicate(rel, entry.name)) out.push(rel);
}
}
}
await walk(root);
out.sort();
return out;
}

function exampleCategory(relativeFromExamples) {
const segments = relativeFromExamples.split("/");
return segments[0] ?? "";
}

function exampleGallerySlug(relativeFromExamples) {
const segments = relativeFromExamples.split("/");
if (segments[0] !== "gallery") return null;
return segments[1] ?? null;
}

function exampleSlug(relativeFromExamples) {
const base = path.posix.basename(relativeFromExamples);
return base.replace(/\.opf\.json$/, "");
}

async function generateExamples() {
const files = await collectFiles(examplesRoot, (rel) => rel.endsWith(".opf.json"));
const examples = [];
for (const file of files) {
const json = JSON.parse(await fs.readFile(path.join(examplesRoot, file), "utf8"));
examples.push({
slug: exampleSlug(file),
file: `examples/${file}`,
category: exampleCategory(file),
gallery: exampleGallerySlug(file),
deck: json,
});
}

const galleries = [];
const galleryMap = new Map();
for (const example of examples) {
if (example.gallery === null) continue;
if (!galleryMap.has(example.gallery)) {
const slug = example.gallery;
const entry = { slug, dir: `examples/gallery/${slug}`, examples: [] };
galleryMap.set(slug, entry);
galleries.push(entry);
}
galleryMap.get(example.gallery).examples.push({
slug: example.slug,
file: example.file,
name: typeof example.deck?.name === "string" ? example.deck.name : null,
});
}
galleries.sort((a, b) => a.slug.localeCompare(b.slug));

const banner = generatedHeader("examples/**/*.opf.json");
const exampleLiteral = JSON.stringify(examples, null, 2);
const galleriesLiteral = JSON.stringify(galleries, null, 2);

const source =
banner +
'import type { Presentation } from "../types.js";\n\n' +
"export interface ExampleRecord {\n" +
" /** Filename slug, without the `.opf.json` suffix. */\n" +
" readonly slug: string;\n" +
" /** Path relative to the repo root, with forward slashes. */\n" +
" readonly file: string;\n" +
" /** Top-level examples/<category> bucket (`gallery`, `technical`, …). */\n" +
" readonly category: string;\n" +
" /** Gallery slug when the example lives under `examples/gallery/<slug>/`; otherwise `null`. */\n" +
" readonly gallery: string | null;\n" +
" /** Parsed OPF document. Validated against `presentation.schema.json` at build time. */\n" +
" readonly deck: Presentation;\n" +
"}\n\n" +
"export interface GalleryRecord {\n" +
" /** Gallery slug (folder name under `examples/gallery/`). */\n" +
" readonly slug: string;\n" +
" /** Directory path relative to the repo root. */\n" +
" readonly dir: string;\n" +
" /** Lightweight summary of every example deck inside the gallery. */\n" +
" readonly examples: readonly { readonly slug: string; readonly file: string; readonly name: string | null }[];\n" +
"}\n\n" +
`const examplesData: readonly ExampleRecord[] = Object.freeze(${exampleLiteral} as readonly ExampleRecord[]);\n\n` +
`const galleriesData: readonly GalleryRecord[] = Object.freeze(${galleriesLiteral} as readonly GalleryRecord[]);\n\n` +
"export const examplesRaw = examplesData;\n" +
"export const galleriesRaw = galleriesData;\n";

await fs.writeFile(path.join(generatedRoot, "examples.ts"), source, "utf8");
console.log(`Generated src/generated/examples.ts (${examples.length} examples, ${galleries.length} galleries)`);
}

function extractFirstHeading(markdown) {
const match = markdown.match(/^#\s+(.+?)\s*$/m);
return match ? match[1].trim() : null;
}

function titleFromSlug(slug) {
return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}

async function generateDocs() {
// docs/*.md only — skip the BACKLOG and any subdirectory until we have a
// separate policy for shipping migration/plan notes inside the package.
const entries = await fs.readdir(docsRoot, { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name !== "BACKLOG.md")
.map((entry) => entry.name)
.sort();

const docs = [];
for (const name of files) {
const slug = name.replace(/\.md$/, "");
const markdown = await fs.readFile(path.join(docsRoot, name), "utf8");
docs.push({
slug,
file: `docs/${name}`,
title: extractFirstHeading(markdown) ?? titleFromSlug(slug),
markdown,
});
}

const banner = generatedHeader("docs/*.md");
const docsLiteral = JSON.stringify(docs, null, 2);
const source =
banner +
"export interface DocRecord {\n" +
" /** Filename slug, without the `.md` suffix. */\n" +
" readonly slug: string;\n" +
" /** Path relative to the repo root, with forward slashes. */\n" +
" readonly file: string;\n" +
" /** First-level heading from the markdown, falling back to a slug-derived title. */\n" +
" readonly title: string;\n" +
" /** Raw markdown content. */\n" +
" readonly markdown: string;\n" +
"}\n\n" +
`const docsData: readonly DocRecord[] = Object.freeze(${docsLiteral} as readonly DocRecord[]);\n\n` +
"export const docsRaw = docsData;\n";

await fs.writeFile(path.join(generatedRoot, "docs.ts"), source, "utf8");
console.log(`Generated src/generated/docs.ts (${docs.length} docs)`);
}

async function generateRepoReadme() {
const markdown = await fs.readFile(readmePath, "utf8");
const banner = generatedHeader("README.md");
const literal = JSON.stringify(markdown);
const source =
banner +
`export const repoReadmeRaw: string = ${literal};\n`;
await fs.writeFile(path.join(generatedRoot, "repo-readme.ts"), source, "utf8");
console.log(`Generated src/generated/repo-readme.ts (${markdown.length} bytes)`);
}

await generateExamples();
await generateDocs();
await generateRepoReadme();
19 changes: 19 additions & 0 deletions packages/javascript/src/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Bundled OPF documentation pages.
//
// Source-of-truth: ../../../docs/*.md (top-level only — `BACKLOG.md` and the
// `docs/migrations/`, `docs/plans/` subdirectories are intentionally excluded
// for now). The build step inlines the raw markdown into this module so
// consumers can render or link to the docs without filesystem access.

import { docsRaw } from "./generated/docs.js";
import type { DocRecord } from "./generated/docs.js";

export type { DocRecord };

/** Every documentation page shipped with this release, sorted by slug. */
export const docs: readonly DocRecord[] = docsRaw;

/** Look up a documentation page by slug (the filename without `.md`). */
export function getDoc(slug: string): DocRecord | undefined {
return docsRaw.find((doc) => doc.slug === slug);
}
47 changes: 47 additions & 0 deletions packages/javascript/src/examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Bundled OPF example decks.
//
// Source-of-truth: ../../../examples/**/*.opf.json. The build step
// (`pnpm build`) inlines each deck into this module so consumers can import
// the gallery without doing any filesystem work at runtime.
//
// Each example's `deck` field is the parsed JSON for an `.opf.json` file —
// the same content you would `JSON.parse(fs.readFileSync(file))`.

import { examplesRaw, galleriesRaw } from "./generated/examples.js";
import type {
ExampleRecord,
GalleryRecord,
} from "./generated/examples.js";

export type { ExampleRecord, GalleryRecord };

/** Every example deck shipped with this release, sorted by file path. */
export const examples: readonly ExampleRecord[] = examplesRaw;

/** Examples grouped by `examples/gallery/<slug>/` folder. */
export const galleries: readonly GalleryRecord[] = galleriesRaw;

/** Top-level `examples/<category>` buckets present in this release. */
export const exampleCategories: readonly string[] = Object.freeze(
Array.from(new Set(examplesRaw.map((example) => example.category))).sort(),
);

/** Look up an example deck by slug (the filename without `.opf.json`). */
export function getExample(slug: string): ExampleRecord | undefined {
return examplesRaw.find((example) => example.slug === slug);
}

/** Look up a gallery by slug. */
export function getGallery(slug: string): GalleryRecord | undefined {
return galleriesRaw.find((gallery) => gallery.slug === slug);
}

/** All examples that belong to a given gallery slug. */
export function getExamplesByGallery(slug: string): readonly ExampleRecord[] {
return examplesRaw.filter((example) => example.gallery === slug);
}

/** All examples that belong to a given top-level category (`gallery`, `technical`, …). */
export function getExamplesByCategory(category: string): readonly ExampleRecord[] {
return examplesRaw.filter((example) => example.category === category);
}
15 changes: 15 additions & 0 deletions packages/javascript/src/repo-readme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// The raw README.md from the OpenPresentation/opf repo at the version pinned
// by this release.
//
// Source-of-truth: ../../../README.md. The build step inlines the markdown
// into this module so consumers can render the upstream README without doing
// any filesystem or network work at runtime.
//
// The package's own README.md (npm landing page) is a separate file — this
// export is intended for sites that want to mirror the canonical upstream
// README.

import { repoReadmeRaw } from "./generated/repo-readme.js";

/** Raw markdown of the upstream OpenPresentation/opf README at this release. */
export const repoReadme: string = repoReadmeRaw;
Loading
Loading