+ );
+}
diff --git a/deno.jsonc b/deno.jsonc
index a52b813..cd7399a 100644
--- a/deno.jsonc
+++ b/deno.jsonc
@@ -29,9 +29,18 @@
"@fartlabs/jsonx": "jsr:@fartlabs/jsonx@^0.0.10",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0",
+ "@std/front-matter": "jsr:@std/front-matter@^0.220.1",
+ "@std/front-matter/any": "jsr:@std/front-matter@^0.220.1/any",
+ "@std/fs": "jsr:@std/fs@^0.220.1",
"@std/http": "jsr:@std/http@^0.220.1",
+ "@std/path": "jsr:@std/path@^0.220.1",
"@std/semver": "jsr:@std/semver@^0.220.1",
+ "@std/ulid": "jsr:@std/ulid@^0.220.1",
"@std/yaml": "jsr:@std/yaml@^0.220.1",
+ "highlight.js": "https://esm.sh/highlight.js@11.9.0",
+ "markdown-it": "https://esm.sh/markdown-it@14.1.0",
+ "markdown-it-anchor": "https://esm.sh/markdown-it-anchor@8.6.7",
+ "markdown-it-toc-done-right": "https://esm.sh/markdown-it-toc-done-right@4.2.0",
"preact": "https://esm.sh/preact@10.19.2",
"preact/": "https://esm.sh/preact@10.19.2/"
},
diff --git a/docs/00_index.md b/docs/00_index.md
new file mode 100644
index 0000000..0d272a1
--- /dev/null
+++ b/docs/00_index.md
@@ -0,0 +1,8 @@
+---
+title: Overview
+playground: 'example:01_animals.tsx'
+---
+
+# Overview
+
+The jsonx library exposes a JSX runtime for composing JSON data.
diff --git a/docs/01_getting_started/00_index.md b/docs/01_getting_started/00_index.md
new file mode 100644
index 0000000..25027f2
--- /dev/null
+++ b/docs/01_getting_started/00_index.md
@@ -0,0 +1,81 @@
+---
+title: Getting started
+---
+
+# Getting Started
+
+Get started with jsonx by following the steps below.
+
+## Install
+
+Install as usual via NPM:
+
+```shell
+npx jsr add @fartlabs/jsonx
+```
+
+Or if you're using Deno:
+
+```shell
+deno add @fartlabs/jsonx
+```
+
+Add the following values to your `deno.json(c)` file.
+
+```json
+{
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxFactory": "@fartlabs/jsonx"
+ }
+}
+```
+
+## Use
+
+Add a file ending in `.[j|t]sx` to your project. For example, `example.tsx`.
+
+```tsx
+function Cat() {
+ return { animals: ["🐈"] };
+}
+
+function Dog() {
+ return { animals: ["🐕"] };
+}
+
+const data = (
+ <>
+
+
+ >
+);
+
+Deno.writeTextFileSync(
+ "data.json",
+ JSON.stringify(data, null, 2),
+);
+```
+
+Compile your jsonx by running the `.[j|t]sx` file.
+
+```shell
+deno run --allow-write example.tsx
+```
+
+Preview the `data.json` file.
+
+```shell
+cat data.json
+```
+
+Resulting `data.json`:
+
+```json
+{
+ "animals": [
+ "🐈",
+ "🐕"
+ ]
+}
+```
diff --git a/docs/02_playgrounds/00_index.md b/docs/02_playgrounds/00_index.md
new file mode 100644
index 0000000..c264dda
--- /dev/null
+++ b/docs/02_playgrounds/00_index.md
@@ -0,0 +1,12 @@
+---
+title: Playgrounds
+---
+
+# Playgrounds
+
+You may encounter text editors across this site with a "Play" button. These are
+playgrounds. Playgrounds enable visitors to quickly edit and run jsonx code in
+the browser.
+
+- [Create a new playground↩](/playgrounds)
+- [Playgrounds handbook↩](/02_playgrounds/02_handbook)
diff --git a/docs/02_playgrounds/01_new.md b/docs/02_playgrounds/01_new.md
new file mode 100644
index 0000000..b6e90d1
--- /dev/null
+++ b/docs/02_playgrounds/01_new.md
@@ -0,0 +1,4 @@
+---
+title: New playground
+href: /playgrounds
+---
diff --git a/docs/02_playgrounds/02_handbook.md b/docs/02_playgrounds/02_handbook.md
new file mode 100644
index 0000000..e556f95
--- /dev/null
+++ b/docs/02_playgrounds/02_handbook.md
@@ -0,0 +1,60 @@
+---
+title: Playgrounds handbook
+---
+
+# Playgrounds Handbook
+
+Playgrounds are useful for trying out jsonx code snippets, sharing code, and
+learning jsonx.
+
+- Edit, transpile, and play playground code in the browser.
+- Share your playground with others.
+
+## Use
+
+To use a playground, let's take a look at the parts that make up one playground.
+
+> [!NOTE]
+>
+> Screenshots of playground parts are coming soon.
+
+### Code editor
+
+The code editor is where you edit code. It is a TypeScript environment that
+comes with batteries-included for editing jsonx code.
+
+### Version dropdown
+
+The version dropdown allows you to select the version of jsonx you want to use.
+The default version is the latest stable version found on
+[jsr.io](https://jsr.io).
+
+### Play button
+
+The play button transpiles and executes the code in the code editor. Output is
+displayed in the provided console.
+
+### Console
+
+The console displays the output of the code executed in the code editor.
+
+The clear button clears the console.
+
+### Share button
+
+The share button generates a link to the playground and navigates the user to
+the new link. The link is unlisted (not indexed by search engines) and can be
+shared with others.
+
+## Recovery and disposal
+
+> [!NOTE]
+>
+> The "Recovery and Disposal" form will be coming soon.
+
+Since playground links are unlisted, losing the link means losing the
+playground. In case you lose a playground, first check your browser history for
+the link. In case the playground is important and unrecoverable, you can use the
+form to file a ticket with the jsonx team so we can help you recover it.
+Similarly, if you have a playground exposing sensitive information, please file
+a ticket so we can remove it.
diff --git a/docs/03_jsx/00_index.md b/docs/03_jsx/00_index.md
new file mode 100644
index 0000000..ba620d8
--- /dev/null
+++ b/docs/03_jsx/00_index.md
@@ -0,0 +1,29 @@
+---
+title: JSX
+---
+
+# JSX
+
+> [!NOTE]
+>
+> Coming soon.
+
+[Learn why jsonx uses JS](/03_jsx/01_theory.md).
+
+### Why does jsonx use JSX?
+
+> [!NOTE]
+>
+> Coming soon.
+
+## Syntax
+
+> [!NOTE]
+>
+> Coming soon.
+
+## Project setup
+
+> [!NOTE]
+>
+> Coming soon.
diff --git a/docs/03_jsx/01_theory.md b/docs/03_jsx/01_theory.md
new file mode 100644
index 0000000..1cf07b1
--- /dev/null
+++ b/docs/03_jsx/01_theory.md
@@ -0,0 +1,15 @@
+---
+title: Theory
+---
+
+# Theory of JSX
+
+> [!NOTE]
+>
+> Coming soon.
+
+## Why does jsonx use JSX?
+
+> [!NOTE]
+>
+> Coming soon.
diff --git a/docs/04_examples/00_index.md b/docs/04_examples/00_index.md
new file mode 100644
index 0000000..81cf421
--- /dev/null
+++ b/docs/04_examples/00_index.md
@@ -0,0 +1,13 @@
+---
+title: Examples
+---
+
+# Examples
+
+> [!NOTE]
+>
+> More examples coming soon.
+
+Learn how to use jsonx in practical situations.
+
+- [Hello world↩](/04_examples/01_hello)
diff --git a/docs/04_examples/01_hello.md b/docs/04_examples/01_hello.md
new file mode 100644
index 0000000..e96d0b7
--- /dev/null
+++ b/docs/04_examples/01_hello.md
@@ -0,0 +1,10 @@
+---
+title: Hello world
+playground: example:01_animals.tsx
+---
+
+# Hello world
+
+This example demonstrates how to use jsonx to create a simple JSON object.
+
+Press the "Play" button to run the code.
diff --git a/docs/98_api_docs.md b/docs/98_api_docs.md
new file mode 100644
index 0000000..5e05d12
--- /dev/null
+++ b/docs/98_api_docs.md
@@ -0,0 +1,4 @@
+---
+title: View API Docs
+href: https://jsr.io/@fartlabs/jsonx
+---
diff --git a/docs/99_github.md b/docs/99_github.md
new file mode 100644
index 0000000..3c186ed
--- /dev/null
+++ b/docs/99_github.md
@@ -0,0 +1,4 @@
+---
+title: View on GitHub
+href: https://github.com/FartLabs/jsonx
+---
diff --git a/server/examples/01_animals.tsx b/examples/01_animals.tsx
similarity index 100%
rename from server/examples/01_animals.tsx
rename to examples/01_animals.tsx
diff --git a/fresh.gen.ts b/fresh.gen.ts
index 9b2bbd7..0ebbe32 100644
--- a/fresh.gen.ts
+++ b/fresh.gen.ts
@@ -2,26 +2,28 @@
// This file SHOULD be checked into source version control.
// This file is automatically updated during development when running `dev.ts`.
+import * as $_path_ from "./routes/[...path].ts";
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
-import * as $api_examples_id_ from "./routes/api/examples/[id].ts";
+import * as $api_examples_name_ from "./routes/api/examples/[name].ts";
+import * as $api_meta from "./routes/api/meta.ts";
import * as $api_playgrounds_id_ from "./routes/api/playgrounds/[id].ts";
import * as $api_playgrounds_index from "./routes/api/playgrounds/index.ts";
-import * as $index from "./routes/index.tsx";
-import * as $meta from "./routes/meta.ts";
+import * as $index from "./routes/index.ts";
import * as $playgrounds_id_ from "./routes/playgrounds/[id].tsx";
import { type Manifest } from "$fresh/server.ts";
const manifest = {
routes: {
+ "./routes/[...path].ts": $_path_,
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
- "./routes/api/examples/[id].ts": $api_examples_id_,
+ "./routes/api/examples/[name].ts": $api_examples_name_,
+ "./routes/api/meta.ts": $api_meta,
"./routes/api/playgrounds/[id].ts": $api_playgrounds_id_,
"./routes/api/playgrounds/index.ts": $api_playgrounds_index,
- "./routes/index.tsx": $index,
- "./routes/meta.ts": $meta,
+ "./routes/index.ts": $index,
"./routes/playgrounds/[id].tsx": $playgrounds_id_,
},
islands: {},
diff --git a/lib/docs/fs.ts b/lib/docs/fs.ts
new file mode 100644
index 0000000..39222b7
--- /dev/null
+++ b/lib/docs/fs.ts
@@ -0,0 +1,185 @@
+import { test } from "@std/front-matter";
+import { extract } from "@std/front-matter/any";
+import { walk } from "@std/fs";
+import { fromFileUrl, normalize, parse, SEPARATOR_PATTERN } from "@std/path";
+import type { FSItem } from "./items.ts";
+import type { Node } from "./nodes.ts";
+import { sortChildren } from "./nodes.ts";
+import { renderMd } from "./md.ts";
+
+/**
+ * RenderFSItemsOptions represents the options for rendering file-based items.
+ */
+export interface ReadFSItemsOptions {
+ root: string | URL;
+ isIndex?: (suffix: string, override?: string) => boolean;
+}
+
+/**
+ * Content represents the content of an item.
+ */
+export interface Content {
+ /**
+ * md is the markdown representation of the content.
+ */
+ md: string;
+
+ /**
+ * html is the HTML representation of the content.
+ */
+ body: string;
+
+ /**
+ * toc is the HTML table of contents of the content.
+ */
+ toc?: string;
+
+ /**
+ * playground is a playground expression.
+ */
+ playground?: string;
+}
+
+/**
+ * ReadFSItemsResult represents the result of reading file-based items.
+ */
+export interface ReadFSItemsResult {
+ items: FSItem[];
+ nodes: Node[];
+ contents: Map;
+}
+
+/**
+ * readFSItems reads the file-based items recursively.
+ */
+export async function readFSItems(
+ options: ReadFSItemsOptions,
+): Promise {
+ function nameOf(path: string, override?: string): string[] {
+ // Parse the path.
+ const parsed = parse(path);
+
+ // Remove index suffix from the name.
+ const current = override ?? parsed.name;
+ const name: string[] = [];
+ if (!options.isIndex?.(parsed.name, override)) {
+ name.push(current);
+ }
+
+ // If the path has a directory, add it to the name.
+ if (parsed.dir !== "") {
+ // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556
+ const parent = parsed.dir
+ .split(SEPARATOR_PATTERN)
+ .slice(root.length);
+ name.unshift(...parent);
+ }
+
+ return name;
+ }
+
+ // Normalize the root.
+ const root = normalize(
+ options.root instanceof URL
+ ? fromFileUrl(options.root.toString())
+ : options.root,
+ ).split(SEPARATOR_PATTERN);
+ if (root[root.length - 1] === "") {
+ root.pop();
+ }
+
+ // Read the file-based items.
+ const items: FSItem[] = [];
+ const contents = new Map();
+ const walkIt = walk(
+ options.root,
+ { exts: [".md"], includeDirs: false },
+ );
+ for await (const file of walkIt) {
+ let md = await Deno.readTextFile(file.path);
+
+ // Render the FSItem.
+ let nameOverride: string | undefined;
+ let title: string | undefined;
+ let href: string | undefined;
+ let playground: string | undefined;
+ if (test(md)) {
+ const extracted = extract<
+ { name: string; title: string; href: string; playground: string }
+ >(md);
+ nameOverride = extracted.attrs.name ?? nameOverride;
+ title = extracted.attrs.title ?? title;
+ href = extracted.attrs.href ?? href;
+ playground = extracted.attrs.playground ?? playground;
+ md = extracted.body;
+ }
+
+ // Get the name of the item.
+ const name = nameOf(file.path, nameOverride);
+
+ // Store the item contents.
+ const { body, toc } = renderMd(md);
+ contents.set(
+ name.join(NAME_SEPARATOR),
+ { md, body, toc, playground },
+ );
+
+ // Store the item in the items array.
+ const item = { name, title, href };
+ items.push(item);
+ }
+
+ // Return items relative to the root.
+ return { items, contents, nodes: toNodes(items) };
+}
+
+/**
+ * NAME_SEPARATOR is the separator for an FSItem name.
+ */
+export const NAME_SEPARATOR = "/";
+
+/**
+ * toNodes converts an array of FSItems to list of tree nodes.
+ */
+export function toNodes(items: FSItem[]): Node[] {
+ const root: Node = { name: [] };
+ let visitedRoot = false;
+ for (const item of items) {
+ let node = root;
+ if (item.name.length === 0) {
+ visitedRoot = true;
+ }
+
+ for (let i = 0; i < item.name.length; i++) {
+ const part = item.name.slice(0, i + 1).join(NAME_SEPARATOR);
+ let child = node.children?.find((child) =>
+ child.name.join(NAME_SEPARATOR) === part
+ );
+ if (child === undefined) {
+ if (node.children === undefined) {
+ node.children = [];
+ }
+
+ child = { name: item.name.slice(0, i + 1) };
+ node.children.push(child);
+ }
+
+ node = child;
+ }
+ node.title = item.title;
+ node.href = item.href;
+ }
+
+ sortChildren(
+ root,
+ (a, b) =>
+ a.name.join(NAME_SEPARATOR).localeCompare(b.name.join(NAME_SEPARATOR)),
+ );
+
+ const children = root.children ?? [];
+ if (visitedRoot) {
+ children.unshift({ name: [], title: root.title, href: root.href });
+ }
+
+ return children;
+}
diff --git a/lib/docs/items.ts b/lib/docs/items.ts
new file mode 100644
index 0000000..1e7ce19
--- /dev/null
+++ b/lib/docs/items.ts
@@ -0,0 +1,8 @@
+/**
+ * FSItem is an item represented in the file system.
+ */
+export interface FSItem {
+ name: string[];
+ title?: string;
+ href?: string;
+}
diff --git a/lib/docs/md.ts b/lib/docs/md.ts
new file mode 100644
index 0000000..3b48ceb
--- /dev/null
+++ b/lib/docs/md.ts
@@ -0,0 +1,50 @@
+import MarkdownIt from "markdown-it";
+import anchorPlugin from "markdown-it-anchor";
+import tocDoneRightPlugin from "markdown-it-toc-done-right";
+import hljs from "highlight.js";
+
+/**
+ * renderMd renders markdown content.
+ */
+export function renderMd(md: string): RenderMdResult {
+ const rendered = renderer.render(`[[toc]]\n\n${md}`);
+ const [toc, body] = rendered.split("");
+ return { body, toc };
+}
+
+/**
+ * RenderMdResult represents the result of rendering markdown content.
+ */
+export interface RenderMdResult {
+ body: string;
+ toc: string;
+}
+
+/**
+ * renderer is the markdown renderer used for rendering markdown content.
+ *
+ * @see
+ * https://github.com/markdown-it/markdown-it/blob/0fe7ccb4b7f30236fb05f623be6924961d296d3d/README.md?plain=1#L154
+ */
+const renderer: MarkdownIt = new MarkdownIt({
+ html: true,
+ linkify: true,
+ typographer: true,
+ highlight(content: string, language?: string) {
+ const html = language && hljs.getLanguage(language)
+ ? hljs.highlight(
+ content,
+ { language, ignoreIllegals: true },
+ ).value
+ : renderer.utils.escapeHtml(content);
+ return `
${html}
`;
+ },
+});
+
+renderer.use(anchorPlugin, {
+ permalink: true,
+ permalinkBefore: true,
+ permalinkSymbol: "§",
+});
+
+renderer.use(tocDoneRightPlugin);
diff --git a/lib/docs/mod_client.ts b/lib/docs/mod_client.ts
new file mode 100644
index 0000000..1952f46
--- /dev/null
+++ b/lib/docs/mod_client.ts
@@ -0,0 +1,2 @@
+export * from "./items.ts";
+export * from "./nodes.ts";
diff --git a/lib/docs/mod_server.ts b/lib/docs/mod_server.ts
new file mode 100644
index 0000000..a2d16b6
--- /dev/null
+++ b/lib/docs/mod_server.ts
@@ -0,0 +1,2 @@
+export * from "./mod_client.ts";
+export * from "./fs.ts";
diff --git a/lib/docs/nodes.ts b/lib/docs/nodes.ts
new file mode 100644
index 0000000..fea8ed3
--- /dev/null
+++ b/lib/docs/nodes.ts
@@ -0,0 +1,19 @@
+/**
+ * Node is a node in a tree.
+ */
+export type Node =
+ & T
+ & { children?: Node[] };
+
+/**
+ * sortChildren sorts the children of a node recursively.
+ */
+export function sortChildren(
+ node: Node,
+ fn: (a: Node, b: Node) => number,
+): void {
+ if (node.children !== undefined) {
+ node.children.sort(fn);
+ node.children.forEach((child) => sortChildren(child, fn));
+ }
+}
diff --git a/lib/examples/examples.ts b/lib/examples/examples.ts
new file mode 100644
index 0000000..a8456c6
--- /dev/null
+++ b/lib/examples/examples.ts
@@ -0,0 +1,24 @@
+/**
+ * readExample reads the example file and returns the content.
+ */
+export async function readExample(path: string | URL): Promise {
+ try {
+ const text = await Deno.readTextFile(path);
+ return trimJSXImportSource(text);
+ } catch (error) {
+ if (error instanceof Deno.errors.NotFound) {
+ return null;
+ }
+
+ throw error;
+ }
+}
+
+function trimJSXImportSource(code: string): string {
+ const jsxImportSource = "/** @jsxImportSource @fartlabs/jsonx */\n\n";
+ if (code.startsWith(jsxImportSource)) {
+ return code.substring(jsxImportSource.length);
+ }
+
+ return code;
+}
diff --git a/server/examples/mod.ts b/lib/examples/mod.ts
similarity index 100%
rename from server/examples/mod.ts
rename to lib/examples/mod.ts
diff --git a/client/meta.ts b/lib/meta/meta.ts
similarity index 73%
rename from client/meta.ts
rename to lib/meta/meta.ts
index 216f79e..12fba7c 100644
--- a/client/meta.ts
+++ b/lib/meta/meta.ts
@@ -24,9 +24,11 @@ function playgroundMeta({ latest, versions }: {
// https://github.com/FartLabs/jsonx/issues/13
const minCompatible = parse("0.0.8");
return {
- latest: latest,
+ latest,
versions: Object.keys(versions)
- .filter((versionTag) => greaterThan(parse(versionTag), minCompatible))
- .sort((a, b) => compare(parse(b), parse(a))),
+ .map((versionTag) => ({ versionTag, semver: parse(versionTag) }))
+ .filter(({ semver }) => greaterThan(semver, minCompatible))
+ .sort((a, b) => compare(b.semver, a.semver))
+ .map((v) => v.versionTag),
};
}
diff --git a/lib/meta/mod.ts b/lib/meta/mod.ts
new file mode 100644
index 0000000..934df4c
--- /dev/null
+++ b/lib/meta/mod.ts
@@ -0,0 +1 @@
+export * from "./meta.ts";
diff --git a/server/playgrounds.ts b/lib/playgrounds/deno_kv/kv.ts
similarity index 79%
rename from server/playgrounds.ts
rename to lib/playgrounds/deno_kv/kv.ts
index 58a5917..32afaa4 100644
--- a/server/playgrounds.ts
+++ b/lib/playgrounds/deno_kv/kv.ts
@@ -1,6 +1,10 @@
///
-import type { AddPlaygroundRequest, Playground } from "#/client/playgrounds.ts";
+import { ulid } from "@std/ulid";
+import type {
+ AddPlaygroundRequest,
+ Playground,
+} from "#/lib/playgrounds/mod.ts";
/**
* getPlayground gets a playground by ID.
@@ -20,7 +24,7 @@ export async function addPlayground(
kv: Deno.Kv,
request: AddPlaygroundRequest,
): Promise {
- const id = crypto.randomUUID();
+ const id = ulid();
const playground = { ...request, id };
const result = await kv.set(playgroundKey(id), playground);
if (!result) {
@@ -43,6 +47,9 @@ export async function setPlayground(
}
}
+// TODO: Add playground title in playground storage via
+// https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt
+
function playgroundKey(id: string): Deno.KvKey {
return ["playgrounds", id];
}
diff --git a/lib/playgrounds/deno_kv/mod.ts b/lib/playgrounds/deno_kv/mod.ts
new file mode 100644
index 0000000..b076734
--- /dev/null
+++ b/lib/playgrounds/deno_kv/mod.ts
@@ -0,0 +1 @@
+export * from "./kv.ts";
diff --git a/lib/playgrounds/expressions/expressions.ts b/lib/playgrounds/expressions/expressions.ts
new file mode 100644
index 0000000..c0ffe07
--- /dev/null
+++ b/lib/playgrounds/expressions/expressions.ts
@@ -0,0 +1,102 @@
+/**
+ * @fileoverview
+ *
+ * This file contains the helper functions for parsing and evaluating playground
+ * expressions. Playground expressions are a simple syntax for embedding
+ * playgrounds directly into markdown files.
+ */
+
+import type { PlaygroundData } from "#/lib/playgrounds/mod.ts";
+import { getPlayground } from "#/lib/playgrounds/deno_kv/mod.ts";
+import { readExample } from "#/lib/examples/mod.ts";
+import { kv } from "#/lib/resources/kv.ts";
+
+/**
+ * PlaygroundExpression is a playground expression.
+ */
+export type PlaygroundExpression =
+ | { id: string }
+ | { example: string };
+
+/**
+ * parsePlaygroundExpression parses a playground expression.
+ *
+ * Example playground expressions:
+ *
+ *
+ *
+ */
+export function parsePlaygroundExpression(
+ expression: string,
+): PlaygroundExpression {
+ const id = parsePlaygroundIDExpression(expression);
+ if (id) {
+ return { id };
+ }
+
+ const example = parsePlaygroundExampleExpression(expression);
+ if (example) {
+ return { example };
+ }
+
+ throw new Error(`Invalid playground expression: ${expression}`);
+}
+
+/**
+ * parsePlaygroundIDExpression parses the playground ID from a playground
+ * expression.
+ */
+export function parsePlaygroundIDExpression(
+ expression: string,
+): string | null {
+ return parseExpressionSuffix(expression, "id:");
+}
+
+/**
+ * parsePlaygroundExampleExpression parses the example name from a playground
+ * expression.
+ */
+export function parsePlaygroundExampleExpression(
+ expression: string,
+): string | null {
+ return parseExpressionSuffix(expression, "example:");
+}
+
+function parseExpressionSuffix(
+ expression: string,
+ prefix: string,
+): string | null {
+ if (expression.startsWith(prefix)) {
+ return expression.slice(prefix.length);
+ }
+
+ return null;
+}
+
+/**
+ * fromExpression converts a playground expression to playground data.
+ */
+export async function fromExpression(
+ expression: string,
+): Promise {
+ const expr = parsePlaygroundExpression(expression);
+ if ("id" in expr) {
+ const playground = await getPlayground(kv, expr.id);
+ if (!playground) {
+ throw new Error(`Playground not found: ${expr.id}`);
+ }
+
+ return { code: playground.code, version: playground.version };
+ }
+
+ if ("example" in expr) {
+ const example = await readExample(`./examples/${expr.example}`);
+ if (!example) {
+ throw new Error(`Example not found: ${expr.example}`);
+ }
+
+ return { code: example };
+ }
+
+ throw new Error(`Invalid playground expression: ${expr}`);
+}
diff --git a/lib/playgrounds/expressions/mod.ts b/lib/playgrounds/expressions/mod.ts
new file mode 100644
index 0000000..845c542
--- /dev/null
+++ b/lib/playgrounds/expressions/mod.ts
@@ -0,0 +1 @@
+export * from "./expressions.ts";
diff --git a/lib/playgrounds/mod.ts b/lib/playgrounds/mod.ts
new file mode 100644
index 0000000..8510bd9
--- /dev/null
+++ b/lib/playgrounds/mod.ts
@@ -0,0 +1 @@
+export * from "./playgrounds.ts";
diff --git a/lib/playgrounds/playgrounds.ts b/lib/playgrounds/playgrounds.ts
new file mode 100644
index 0000000..baaf663
--- /dev/null
+++ b/lib/playgrounds/playgrounds.ts
@@ -0,0 +1,34 @@
+/**
+ * Playground is a stored jsonx playground.
+ */
+export interface Playground extends PlaygroundData {
+ /**
+ * id is the playground ID.
+ */
+ id: string;
+
+ /**
+ * version is the playground version.
+ */
+ version: string;
+}
+
+/**
+ * PlaygroundData is the data for a playground.
+ */
+export interface PlaygroundData {
+ /**
+ * code is the playground code.
+ */
+ code: string;
+
+ /**
+ * version is the playground version.
+ */
+ version?: string;
+}
+
+/**
+ * AddPlaygroundRequest is the request to add a playground.
+ */
+export type AddPlaygroundRequest = Omit;
diff --git a/lib/resources/docs.ts b/lib/resources/docs.ts
new file mode 100644
index 0000000..4b7096d
--- /dev/null
+++ b/lib/resources/docs.ts
@@ -0,0 +1,6 @@
+import { readFSItems } from "#/lib/docs/mod_server.ts";
+
+export const { items, contents, nodes } = await readFSItems({
+ root: "./docs",
+ isIndex: (suffix) => suffix.startsWith("00_"),
+});
diff --git a/lib/resources/examples.ts b/lib/resources/examples.ts
new file mode 100644
index 0000000..2192601
--- /dev/null
+++ b/lib/resources/examples.ts
@@ -0,0 +1,3 @@
+import { readExample } from "#/lib/examples/examples.ts";
+
+export const defaultExample = (await readExample("./examples/01_animals.tsx"))!;
diff --git a/server/kv.ts b/lib/resources/kv.ts
similarity index 100%
rename from server/kv.ts
rename to lib/resources/kv.ts
diff --git a/lib/to_path.ts b/lib/to_path.ts
new file mode 100644
index 0000000..8473b96
--- /dev/null
+++ b/lib/to_path.ts
@@ -0,0 +1,6 @@
+/**
+ * toPath returns the path from the path string.
+ */
+export function toPath(path: string): string[] {
+ return path.split("/").filter(Boolean);
+}
diff --git a/routes/[...path].ts b/routes/[...path].ts
new file mode 100644
index 0000000..a2e961a
--- /dev/null
+++ b/routes/[...path].ts
@@ -0,0 +1 @@
+export { default as default } from "#/components/doc/doc.tsx";
diff --git a/routes/_404.tsx b/routes/_404.tsx
index c63ae2e..e7a38b5 100644
--- a/routes/_404.tsx
+++ b/routes/_404.tsx
@@ -8,13 +8,6 @@ export default function Error404() {
-
404 - Page not found
The page you were looking for doesn't exist.
diff --git a/routes/_app.tsx b/routes/_app.tsx
index 2748bee..b51a12f 100644
--- a/routes/_app.tsx
+++ b/routes/_app.tsx
@@ -1,15 +1,23 @@
-import { type PageProps } from "$fresh/server.ts";
+import type { PageProps } from "$fresh/server.ts";
+import { toPath } from "#/lib/to_path.ts";
+import Nav from "#/components/nav.tsx";
+import Foot from "#/components/foot.tsx";
-export default function App({ Component }: PageProps) {
+export default function App({ Component, url }: PageProps) {
return (