From 16de4e78bf1f7df373cf05fb7177d33feba94ca8 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:52:14 -0700 Subject: [PATCH 01/34] add overview docs content --- server/docs/index.md | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 server/docs/index.md diff --git a/server/docs/index.md b/server/docs/index.md new file mode 100644 index 0000000..ae67b79 --- /dev/null +++ b/server/docs/index.md @@ -0,0 +1,95 @@ +--- +title: Overview +--- + +# Overview + +The jsonx library exposes a JSX runtime for composing JSON data. + +## Install + +Install as usual via NPM: + +```sh +npx jsr add @fartlabs/jsonx +``` + +Or if you're using Deno: + +```sh +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. + +```sh +deno run --allow-write example.tsx +``` + +Preview the `data.json` file. + +```sh +cat data.json +``` + +Resulting `data.json`: + +```json +{ + "animals": [ + "🐈", + "πŸ•" + ] +} +``` + +## Motivation + +Optimize developer ergonomics with improved modularity and maintainability by +enabling developers to compose JSON data like React components, using JSX. + +## Contribute + +Contributions are welcome! Check out the relevant GitHub repositories: + +- [FartLabs/jsonx](https://github.com/FartLabs/jsonx): JSX runtime for composing + JSON data. +- [FartLabs/jsonx_docs](https://github.com/FartLabs/jsonx_docs): Documentation + site for the jsonx runtime. From da50e4e2bca3d4fa04a239755452e701c281f6b0 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Tue, 19 Mar 2024 18:15:53 -0700 Subject: [PATCH 02/34] toc wip --- deno.jsonc | 4 + server/docs/01_getting_started/01_install.md | 28 +++++ server/docs/01_getting_started/02_use.md | 52 +++++++++ server/docs/01_getting_started/index.md | 5 + server/docs/98_motivation.md | 8 ++ server/docs/99_view_on_github.md | 4 + server/docs/docs.ts | 112 +++++++++++++++++++ server/docs/index.md | 88 --------------- server/docs/mod.ts | 1 + wip.ts | 8 ++ 10 files changed, 222 insertions(+), 88 deletions(-) create mode 100644 server/docs/01_getting_started/01_install.md create mode 100644 server/docs/01_getting_started/02_use.md create mode 100644 server/docs/01_getting_started/index.md create mode 100644 server/docs/98_motivation.md create mode 100644 server/docs/99_view_on_github.md create mode 100644 server/docs/docs.ts create mode 100644 server/docs/mod.ts create mode 100644 wip.ts diff --git a/deno.jsonc b/deno.jsonc index a52b813..aeddca6 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -26,10 +26,14 @@ "#/": "./", "$fresh/": "https://deno.land/x/fresh@1.6.5/", "$std/": "https://deno.land/std@0.211.0/", + "@deno/gfm": "jsr:@deno/gfm@^0.8.0", "@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/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/yaml": "jsr:@std/yaml@^0.220.1", "preact": "https://esm.sh/preact@10.19.2", diff --git a/server/docs/01_getting_started/01_install.md b/server/docs/01_getting_started/01_install.md new file mode 100644 index 0000000..95d1c0b --- /dev/null +++ b/server/docs/01_getting_started/01_install.md @@ -0,0 +1,28 @@ +--- +title: Install +--- + +## Install + +Install as usual via NPM: + +```sh +npx jsr add @fartlabs/jsonx +``` + +Or if you're using Deno: + +```sh +deno add @fartlabs/jsonx +``` + +Add the following values to your `deno.json(c)` file. + +```json +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxFactory": "@fartlabs/jsonx" + } +} +``` diff --git a/server/docs/01_getting_started/02_use.md b/server/docs/01_getting_started/02_use.md new file mode 100644 index 0000000..bfe305e --- /dev/null +++ b/server/docs/01_getting_started/02_use.md @@ -0,0 +1,52 @@ +--- +title: Use +--- + +## 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. + +```sh +deno run --allow-write example.tsx +``` + +Preview the `data.json` file. + +```sh +cat data.json +``` + +Resulting `data.json`: + +```json +{ + "animals": [ + "🐈", + "πŸ•" + ] +} +``` diff --git a/server/docs/01_getting_started/index.md b/server/docs/01_getting_started/index.md new file mode 100644 index 0000000..39a8986 --- /dev/null +++ b/server/docs/01_getting_started/index.md @@ -0,0 +1,5 @@ +--- +title: Getting Started +--- + +# Getting Started diff --git a/server/docs/98_motivation.md b/server/docs/98_motivation.md new file mode 100644 index 0000000..be8ee52 --- /dev/null +++ b/server/docs/98_motivation.md @@ -0,0 +1,8 @@ +--- +title: Motivation +--- + +## Motivation + +Optimize developer ergonomics with improved modularity and maintainability by +enabling developers to compose JSON data like React components, using JSX. diff --git a/server/docs/99_view_on_github.md b/server/docs/99_view_on_github.md new file mode 100644 index 0000000..3c186ed --- /dev/null +++ b/server/docs/99_view_on_github.md @@ -0,0 +1,4 @@ +--- +title: View on GitHub +href: https://github.com/FartLabs/jsonx +--- diff --git a/server/docs/docs.ts b/server/docs/docs.ts new file mode 100644 index 0000000..c5c8ce4 --- /dev/null +++ b/server/docs/docs.ts @@ -0,0 +1,112 @@ +import { extract } from "@std/front-matter/any"; +import { test } from "@std/front-matter/test"; +import type { RenderOptions } from "@deno/gfm"; +import { render } from "@deno/gfm"; +import { expandGlob } from "@std/fs/expand_glob"; +import { fromFileUrl, join, parse, relative } from "@std/path"; + +/** + * Docs represents a documentation page. + */ +export type Docs = + & { name: string; title: string } + & ( + | { md: string; html: string } + | { href: string } + ); + +/** + * getDocsByName gets the content of a markdown file by name. + */ +export async function getDocsByName( + name: string, + renderOptions?: RenderOptions, +): Promise { + const content = await Deno.readTextFile( + new URL(import.meta.resolve("./" + name)), + ); + return parseDocs({ name, content, renderOptions }); +} + +/** + * DocsInput represents the options for parsing documentation. + */ +export interface DocsInput { + name: string; + content: string; + renderOptions?: RenderOptions; +} + +/** + * parseDocs parses the documentation from a markdown file. + */ +export function parseDocs(input: DocsInput): Docs { + let title = input.name; + let md = input.content; + if (test(input.content)) { + const extracted = extract<{ title: string; href: string }>(input.content); + title = extracted.attrs.title || title; + md = extracted.body; + if (extracted.attrs.href) { + return { name: input.name, title, href: extracted.attrs.href }; + } + } + + const html = render(md, input.renderOptions); + return { name: input.name, title, md, html }; +} + +/** + * TableOfContentsChild represents a child of the table of contents. + */ +export interface TableOfContentsChild { + name: string[]; + title: string; + + /** + * href is a custom documentation link. + */ + href?: string; +} + +/** + * TableOfContents represents the table of contents for the documentation. + */ +export interface TableOfContents { + children: ( + & TableOfContentsChild + & { children?: TableOfContentsChild[] } + )[]; +} + +/** + * getTableOfContents gets the file-based table of contents. + */ +export async function getTableOfContents(): Promise { + const children: TableOfContentsChild[] = []; + for await (const file of expandGlob(new URL("**/*.md", import.meta.url))) { + const path = parse( + relative(join(fromFileUrl(import.meta.url), "../"), file.path), + ); + const content = await Deno.readTextFile(file.path); + const docs = parseDocs({ name: path.base, content }); + const child: TableOfContentsChild = { + name: [path.base], + title: docs.title, + }; + if (path.dir !== "") { + child.name.unshift(path.dir); + } + + if ("href" in docs) { + child.href = docs.href; + } + + children.push(child); + } + + console.log({ children }); + const toc: TableOfContents = { children: [] }; + + return toc; +} diff --git a/server/docs/index.md b/server/docs/index.md index ae67b79..beb6f19 100644 --- a/server/docs/index.md +++ b/server/docs/index.md @@ -5,91 +5,3 @@ title: Overview # Overview The jsonx library exposes a JSX runtime for composing JSON data. - -## Install - -Install as usual via NPM: - -```sh -npx jsr add @fartlabs/jsonx -``` - -Or if you're using Deno: - -```sh -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. - -```sh -deno run --allow-write example.tsx -``` - -Preview the `data.json` file. - -```sh -cat data.json -``` - -Resulting `data.json`: - -```json -{ - "animals": [ - "🐈", - "πŸ•" - ] -} -``` - -## Motivation - -Optimize developer ergonomics with improved modularity and maintainability by -enabling developers to compose JSON data like React components, using JSX. - -## Contribute - -Contributions are welcome! Check out the relevant GitHub repositories: - -- [FartLabs/jsonx](https://github.com/FartLabs/jsonx): JSX runtime for composing - JSON data. -- [FartLabs/jsonx_docs](https://github.com/FartLabs/jsonx_docs): Documentation - site for the jsonx runtime. diff --git a/server/docs/mod.ts b/server/docs/mod.ts new file mode 100644 index 0000000..fed613c --- /dev/null +++ b/server/docs/mod.ts @@ -0,0 +1 @@ +export * from "./docs.ts"; diff --git a/wip.ts b/wip.ts new file mode 100644 index 0000000..a03729e --- /dev/null +++ b/wip.ts @@ -0,0 +1,8 @@ +import { getTableOfContents } from "#/server/docs/mod.ts"; + +// deno run -A wip.ts +// +if (import.meta.main) { + const toc = await getTableOfContents(); + console.log({ toc }); +} From e5778fe299f628c5c995dd7ece9a93bbfc41c5bc Mon Sep 17 00:00:00 2001 From: EthanThatOneKid <31261035+EthanThatOneKid@users.noreply.github.com> Date: Tue, 19 Mar 2024 18:18:36 -0700 Subject: [PATCH 03/34] move nav to components dir --- client/{ => components}/nav.tsx | 0 routes/index.tsx | 2 +- routes/playgrounds/[id].tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename client/{ => components}/nav.tsx (100%) diff --git a/client/nav.tsx b/client/components/nav.tsx similarity index 100% rename from client/nav.tsx rename to client/components/nav.tsx diff --git a/routes/index.tsx b/routes/index.tsx index 8fcb3e8..ca07b6e 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -1,6 +1,6 @@ import { Head } from "$fresh/runtime.ts"; import Playground from "#/client/components/playground/playground.tsx"; -import Nav from "#/client/nav.tsx"; +import Nav from "#/client/components/nav.tsx"; import { getMeta } from "#/client/meta.ts"; import { kv } from "#/server/kv.ts"; import { getExampleByName } from "#/server/examples/mod.ts"; diff --git a/routes/playgrounds/[id].tsx b/routes/playgrounds/[id].tsx index d9a8d44..6badc16 100644 --- a/routes/playgrounds/[id].tsx +++ b/routes/playgrounds/[id].tsx @@ -1,6 +1,6 @@ import type { FreshContext } from "$fresh/server.ts"; import { Head } from "$fresh/runtime.ts"; -import Nav from "#/client/nav.tsx"; +import Nav from "#/client/components/nav.tsx"; import Playground from "#/client/components/playground/playground.tsx"; import { getMeta } from "#/client/meta.ts"; import { getPlayground } from "#/server/playgrounds.ts"; From d1e131b580a91df9fda5fb20186c1daafe18e8b3 Mon Sep 17 00:00:00 2001 From: EthanThatOneKid <31261035+EthanThatOneKid@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:25:18 -0700 Subject: [PATCH 04/34] wip --- server/docs/{index.md => 00_index.md} | 0 .../{index.md => 00_index.md} | 0 server/docs/98_motivation.md | 8 ---- server/docs/docs.ts | 41 +++++++++++-------- 4 files changed, 23 insertions(+), 26 deletions(-) rename server/docs/{index.md => 00_index.md} (100%) rename server/docs/01_getting_started/{index.md => 00_index.md} (100%) delete mode 100644 server/docs/98_motivation.md diff --git a/server/docs/index.md b/server/docs/00_index.md similarity index 100% rename from server/docs/index.md rename to server/docs/00_index.md diff --git a/server/docs/01_getting_started/index.md b/server/docs/01_getting_started/00_index.md similarity index 100% rename from server/docs/01_getting_started/index.md rename to server/docs/01_getting_started/00_index.md diff --git a/server/docs/98_motivation.md b/server/docs/98_motivation.md deleted file mode 100644 index be8ee52..0000000 --- a/server/docs/98_motivation.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Motivation ---- - -## Motivation - -Optimize developer ergonomics with improved modularity and maintainability by -enabling developers to compose JSON data like React components, using JSX. diff --git a/server/docs/docs.ts b/server/docs/docs.ts index c5c8ce4..0b60954 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -2,14 +2,14 @@ import { extract } from "@std/front-matter/any"; import { test } from "@std/front-matter/test"; import type { RenderOptions } from "@deno/gfm"; import { render } from "@deno/gfm"; -import { expandGlob } from "@std/fs/expand_glob"; -import { fromFileUrl, join, parse, relative } from "@std/path"; +import { walk } from "@std/fs/walk"; +import { join, parse, SEPARATOR_PATTERN } from "@std/path"; /** * Docs represents a documentation page. */ export type Docs = - & { name: string; title: string } + & { name: string[]; title: string } & ( | { md: string; html: string } | { href: string } @@ -19,11 +19,11 @@ export type Docs = * getDocsByName gets the content of a markdown file by name. */ export async function getDocsByName( - name: string, + name: string[], renderOptions?: RenderOptions, ): Promise { const content = await Deno.readTextFile( - new URL(import.meta.resolve("./" + name)), + `./server/docs/${name.join("/")}.md`, ); return parseDocs({ name, content, renderOptions }); } @@ -32,7 +32,7 @@ export async function getDocsByName( * DocsInput represents the options for parsing documentation. */ export interface DocsInput { - name: string; + name: string[]; content: string; renderOptions?: RenderOptions; } @@ -41,7 +41,7 @@ export interface DocsInput { * parseDocs parses the documentation from a markdown file. */ export function parseDocs(input: DocsInput): Docs { - let title = input.name; + let title = ""; let md = input.content; if (test(input.content)) { const extracted = extract<{ title: string; href: string }>(input.content); @@ -82,20 +82,25 @@ export interface TableOfContents { /** * getTableOfContents gets the file-based table of contents. */ -export async function getTableOfContents(): Promise { +export async function getTableOfContents( + root = ["server", "docs"], +): Promise { const children: TableOfContentsChild[] = []; - for await (const file of expandGlob(new URL("**/*.md", import.meta.url))) { - const path = parse( - relative(join(fromFileUrl(import.meta.url), "../"), file.path), - ); + const walkIt = walk(join(...root), { exts: [".md"] }); + for await (const file of walkIt) { + if (file.isDirectory) { + continue; + } + + const path = parse(file.path); + const name = [path.name]; const content = await Deno.readTextFile(file.path); - const docs = parseDocs({ name: path.base, content }); - const child: TableOfContentsChild = { - name: [path.base], - title: docs.title, - }; + const docs = parseDocs({ name, content }); + const child: TableOfContentsChild = { name, title: docs.title }; if (path.dir !== "") { - child.name.unshift(path.dir); + // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556 + const parents = path.dir.split(SEPARATOR_PATTERN).slice(root.length); + child.name.unshift(...parents); } if ("href" in docs) { From 91de5da960b2704bcbbf283ea3b8b268c5847549 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:00:30 -0700 Subject: [PATCH 05/34] wip --- server/docs/docs.ts | 181 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 166 insertions(+), 15 deletions(-) diff --git a/server/docs/docs.ts b/server/docs/docs.ts index 0b60954..8418f75 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -57,9 +57,9 @@ export function parseDocs(input: DocsInput): Docs { } /** - * TableOfContentsChild represents a child of the table of contents. + * FSItem is an item represented in the file system. */ -export interface TableOfContentsChild { +export interface FSItem { name: string[]; title: string; @@ -73,10 +73,7 @@ export interface TableOfContentsChild { * TableOfContents represents the table of contents for the documentation. */ export interface TableOfContents { - children: ( - & TableOfContentsChild - & { children?: TableOfContentsChild[] } - )[]; + children: Node[]; } /** @@ -85,18 +82,14 @@ export interface TableOfContents { export async function getTableOfContents( root = ["server", "docs"], ): Promise { - const children: TableOfContentsChild[] = []; - const walkIt = walk(join(...root), { exts: [".md"] }); + const children: FSItem[] = []; + const walkIt = walk(join(...root), { exts: [".md"], includeDirs: false }); for await (const file of walkIt) { - if (file.isDirectory) { - continue; - } - const path = parse(file.path); const name = [path.name]; const content = await Deno.readTextFile(file.path); const docs = parseDocs({ name, content }); - const child: TableOfContentsChild = { name, title: docs.title }; + const child: FSItem = { name, title: docs.title }; if (path.dir !== "") { // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556 const parents = path.dir.split(SEPARATOR_PATTERN).slice(root.length); @@ -110,8 +103,166 @@ export async function getTableOfContents( children.push(child); } - console.log({ children }); - const toc: TableOfContents = { children: [] }; + return { children }; +} + +export type Node = + & T + & { children?: Node[] }; + +function sortChildren(node: Node, fn: (node: Node) => number): void { + if (node.children !== undefined) { + node.children.sort(fn); + node.children.forEach((child) => sortChildren(child, fn)); + } +} + +// function dfs_sort(entry) { +// // If current node has children, sort them +// if (entry.children !== undefined) { +// // Write a sort function that sorts by string order of the first element in the name field +// //entry.children.sort((a, b) => a.name[0].localeCompare(b.name[0])); +// entry.children.sort((a, b) => +// a.name.join("/").localeCompare(b.name.join("/")) +// ); +// } + +// // For each child, if they have children, sort them +// for (let i = 0; i < entry.children.length; i++) { +// if (entry.children[i].children !== undefined) { +// dfs_sort(entry.children[i]); +// } +// } +// } + +const rootKey = "-"; + +function makeAdjacencies( + children: FSItem[], +): { [key: string]: Node[] } { + // Store an adjacency list of the sections and their paths. + const adjacencies: { [key: string]: Node[] } = { [rootKey]: [] }; + + // Loop through children and store them in the adjacency list. + for ( + const item of children.toSorted((a, b) => a.name.length - b.name.length) + ) { + const suffix = item.name[item.name.length - 1]; + if (suffix.startsWith("00") && item.name.length !== 1) { + item.name.pop(); + adjacencies[rootKey].push(item); + continue; + } + + let path = item.name.slice(0, -1).join("/"); + path = path === "" ? rootKey : path; + if (adjacencies[path] === undefined) { + adjacencies[path] = []; + } + + adjacencies[path].push(item); + } + + return adjacencies; +} + +function makeTableOfContents( + children: FSItem[], +): TableOfContents { + const adjacencies = makeAdjacencies(children); + console.log({ adjacencies }); + + // Loop through the adjacencies and build the table of contents. + const paths = Object.keys(adjacencies) + .toSorted((b, a) => a.split("/").length - b.split("/").length); + for (const path of paths) { + // Save the root path for last. + if (path === rootKey) { + continue; + } + + // Get parent path. + const parentPath = path.split("/").slice(0, -1).join("/"); + + // Find parent. + const parent = adjacencies[parentPath] + ?.find((item) => item.name.join("/") === path); + if (parent === undefined) { + continue; + } + + // Add children to parent. + parent.children = adjacencies[path]; + delete adjacencies[path]; + } + + // Add virtual root to children. + for (const item of adjacencies[rootKey]) { + // WIP: Suffix '00_index' should be omitted from name. + } + console.log({ adjacencies }); + const toc = { children: [] }; return toc; } + +// deno run -A server/docs/docs.ts +// +if (import.meta.main) { + // TODO: Rename to getFSItems. + const toc = await getTableOfContents(); + // console.log(toc); + // TODO: Rename to getTableOfContents. + const toc2 = makeTableOfContents(toc.children); + // console.log(toc2); +} + +/* + // We now have a hashmap of "path/to/a/section" => [section_entry] + // Where a section_entry is an object with it's data: {name: ["path", "to", "section"], title: "Section Title", Optional[children]: [], Optional[href]: "https://example.com"} + + // Now loop through our entries by depth + const paths = Object.keys(sections); + paths.sort((b, a) => a.split("/").length - b.split("/").length); + for (let i = 0; i < paths.length; i++) { + // Save the root path for last + if (paths[i] === "") continue; + // Get what our parent's "name" field would be + const parent_name_field = paths[i].split("/"); + // Get where our parent's path would be stored + let parent_path = ""; + if (parent_name_field.length !== 1) { + parent_path = parent_name_field.slice(0, -1).join("/"); + } + // Search for our parent + for (let j = 0; j < sections[parent_path].length; j++) { + // If their field matches what would be our field + if ( + sections[parent_path][j].name.join("/") == parent_name_field.join("/") + ) { + // Add our children to our parent + sections[parent_path][j].children = sections[paths[i]]; + // Remove our children from the list of sections + delete sections[paths[i]]; + break; + } else { + } + } + } + + // Finally, re_add the "00_index" to children at root + for (let i = 0; i < sections[""].length; i++) { + const name_field = sections[""][i].name; + if (name_field[0] === "00_index" || name_field[0].slice(0, 2) == "99") { + continue; + } else { + name_field.push("00_index"); + } + } + + // Sort titles + const ret = { children: sections[""] }; + dfs_sort(ret); + return ret; +} +*/ From 604073b5a6c8fae5e85aaaf4825bf2aa56fc3117 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Thu, 21 Mar 2024 00:49:34 -0700 Subject: [PATCH 06/34] wip --- server/docs/docs.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/docs/docs.ts b/server/docs/docs.ts index 8418f75..6b91739 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -169,8 +169,9 @@ function makeAdjacencies( function makeTableOfContents( children: FSItem[], ): TableOfContents { + console.log({ children }); const adjacencies = makeAdjacencies(children); - console.log({ adjacencies }); + // console.log({ adjacencies }); // Loop through the adjacencies and build the table of contents. const paths = Object.keys(adjacencies) @@ -201,7 +202,7 @@ function makeTableOfContents( // WIP: Suffix '00_index' should be omitted from name. } - console.log({ adjacencies }); + // console.log({ adjacencies }); const toc = { children: [] }; return toc; } From 555031ea02126f30cf8673549d53a81551734848 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:33:37 -0700 Subject: [PATCH 07/34] wip --- server/docs/docs.ts | 333 +++++++++++++++++--------------------------- wip.ts | 8 -- 2 files changed, 126 insertions(+), 215 deletions(-) delete mode 100644 wip.ts diff --git a/server/docs/docs.ts b/server/docs/docs.ts index 6b91739..70d829a 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -6,54 +6,23 @@ import { walk } from "@std/fs/walk"; import { join, parse, SEPARATOR_PATTERN } from "@std/path"; /** - * Docs represents a documentation page. + * renderFSItem renders the FSItem. */ -export type Docs = - & { name: string[]; title: string } - & ( - | { md: string; html: string } - | { href: string } - ); - -/** - * getDocsByName gets the content of a markdown file by name. - */ -export async function getDocsByName( - name: string[], - renderOptions?: RenderOptions, -): Promise { - const content = await Deno.readTextFile( - `./server/docs/${name.join("/")}.md`, - ); - return parseDocs({ name, content, renderOptions }); -} - -/** - * DocsInput represents the options for parsing documentation. - */ -export interface DocsInput { - name: string[]; - content: string; - renderOptions?: RenderOptions; -} +export function renderFSItem(name: string[], md: string): FSItem { + let title: string | undefined; + let href: string | undefined; + if (test(md)) { + const extracted = extract<{ title: string; href: string }>(md); + if (extracted.attrs.title !== undefined) { + title = extracted.attrs.title; + } -/** - * parseDocs parses the documentation from a markdown file. - */ -export function parseDocs(input: DocsInput): Docs { - let title = ""; - let md = input.content; - if (test(input.content)) { - const extracted = extract<{ title: string; href: string }>(input.content); - title = extracted.attrs.title || title; - md = extracted.body; - if (extracted.attrs.href) { - return { name: input.name, title, href: extracted.attrs.href }; + if (extracted.attrs.href !== undefined) { + href = extracted.attrs.href; } } - const html = render(md, input.renderOptions); - return { name: input.name, title, md, html }; + return { name, title, href }; } /** @@ -61,209 +30,159 @@ export function parseDocs(input: DocsInput): Docs { */ export interface FSItem { name: string[]; - title: string; - - /** - * href is a custom documentation link. - */ + title?: string; href?: string; } /** - * TableOfContents represents the table of contents for the documentation. + * RenderFSItemsOptions represents the options for rendering file-based items. */ -export interface TableOfContents { - children: Node[]; +export interface ReadFSItemsOptions { + root: string[]; + isIndex?: (suffix: string) => boolean; } /** - * getTableOfContents gets the file-based table of contents. + * readFSItems reads the file-based items recursively. */ -export async function getTableOfContents( - root = ["server", "docs"], -): Promise { - const children: FSItem[] = []; - const walkIt = walk(join(...root), { exts: [".md"], includeDirs: false }); +export async function readFSItems( + options: ReadFSItemsOptions, +): Promise { + const items: FSItem[] = []; + const walkIt = walk( + join(...options.root), + { exts: [".md"], includeDirs: false }, + ); for await (const file of walkIt) { + const md = await Deno.readTextFile(file.path); const path = parse(file.path); - const name = [path.name]; - const content = await Deno.readTextFile(file.path); - const docs = parseDocs({ name, content }); - const child: FSItem = { name, title: docs.title }; + // Remove index suffix from the name. + const name = options.isIndex?.(path.name) ? [] : [path.name]; + + // If the path has a directory, add it to the name. if (path.dir !== "") { // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556 - const parents = path.dir.split(SEPARATOR_PATTERN).slice(root.length); - child.name.unshift(...parents); + const parents = path.dir + .split(SEPARATOR_PATTERN) + .slice(options.root.length); + name.unshift(...parents); } - if ("href" in docs) { - child.href = docs.href; - } - - children.push(child); + const item = renderFSItem(name, md); + items.push(item); } - return { children }; + return items; } -export type Node = - & T - & { children?: Node[] }; - -function sortChildren(node: Node, fn: (node: Node) => number): void { - if (node.children !== undefined) { - node.children.sort(fn); - node.children.forEach((child) => sortChildren(child, fn)); - } -} +// function adjacenciesOf(items: FSItem[]): Map { +// const adjacencies = new Map(); +// for (const item of items) { +// const parent = item.name.slice(0, -1); +// if (!adjacencies.has(parent)) { +// adjacencies.set(parent, []); +// } -// function dfs_sort(entry) { -// // If current node has children, sort them -// if (entry.children !== undefined) { -// // Write a sort function that sorts by string order of the first element in the name field -// //entry.children.sort((a, b) => a.name[0].localeCompare(b.name[0])); -// entry.children.sort((a, b) => -// a.name.join("/").localeCompare(b.name.join("/")) -// ); +// adjacencies.get(parent)!.push(item.name[item.name.length - 1]); // } -// // For each child, if they have children, sort them -// for (let i = 0; i < entry.children.length; i++) { -// if (entry.children[i].children !== undefined) { -// dfs_sort(entry.children[i]); -// } -// } +// return adjacencies; // } -const rootKey = "-"; - -function makeAdjacencies( - children: FSItem[], -): { [key: string]: Node[] } { - // Store an adjacency list of the sections and their paths. - const adjacencies: { [key: string]: Node[] } = { [rootKey]: [] }; - - // Loop through children and store them in the adjacency list. - for ( - const item of children.toSorted((a, b) => a.name.length - b.name.length) - ) { - const suffix = item.name[item.name.length - 1]; - if (suffix.startsWith("00") && item.name.length !== 1) { - item.name.pop(); - adjacencies[rootKey].push(item); - continue; +const ROOT_PARENT = "-"; +const NAME_SEPARATOR = "/"; + +function parentRelationshipsOf(items: FSItem[]): Map { + const relationships = new Map(); + for (const item of items) { + const parentKey = + (item.name.length > 1 ? item.name.slice(0, -1) : [ROOT_PARENT]) + .join(NAME_SEPARATOR); + if (!relationships.has(parentKey)) { + relationships.set(parentKey, []); } - let path = item.name.slice(0, -1).join("/"); - path = path === "" ? rootKey : path; - if (adjacencies[path] === undefined) { - adjacencies[path] = []; - } - - adjacencies[path].push(item); + relationships.get(parentKey)!.push(item.name); } - return adjacencies; + return relationships; } -function makeTableOfContents( - children: FSItem[], -): TableOfContents { - console.log({ children }); - const adjacencies = makeAdjacencies(children); - // console.log({ adjacencies }); +export type Node = + & T + & { children?: Node[] }; - // Loop through the adjacencies and build the table of contents. - const paths = Object.keys(adjacencies) - .toSorted((b, a) => a.split("/").length - b.split("/").length); - for (const path of paths) { - // Save the root path for last. - if (path === rootKey) { - continue; - } +// function sortChildren(node: Node, fn: (node: Node) => number): void { +// if (node.children !== undefined) { +// node.children.sort(fn); +// node.children.forEach((child) => sortChildren(child, fn)); +// } +// } - // Get parent path. - const parentPath = path.split("/").slice(0, -1).join("/"); +function tocOf(input: FSItem[]): Node[] { + // Get the parent relationships of the input. + const parentRelationships = parentRelationshipsOf(input); - // Find parent. - const parent = adjacencies[parentPath] - ?.find((item) => item.name.join("/") === path); - if (parent === undefined) { - continue; - } + // Construct the table of contents. + const children: Node[] = []; - // Add children to parent. - parent.children = adjacencies[path]; - delete adjacencies[path]; - } + // function appendChildren(name: string[]) { + // // Add the item to the table of contents. + // const key = name.join(NAME_SEPARATOR); + // const item = input.find((item) => item.name.join(NAME_SEPARATOR) === key); + // let node: Node= + // while (node.children !== undefined) { + // } - // Add virtual root to children. - for (const item of adjacencies[rootKey]) { - // WIP: Suffix '00_index' should be omitted from name. - } + // // if (parentRelationships.has(key)) { + // // } + // } + + // appendChildren([ROOT_PARENT]); + console.log({ children }); - // console.log({ adjacencies }); - const toc = { children: [] }; - return toc; + // const memo = new Map(); + // while (true) { + // for (const child of children) { + // if (!memo.has(child.name)) { + // memo.set(child.name, []); + // } + + // Iterate over top level of children. + // For each child, check if it has a parent. + // children.forEach((child) => { + // if (memo.has(child.name)) { + // If a child has a parent, add it to the parent's children array. + + // If a child has no parent, add it to the top level of the table of contents. + // } + // + // + //// memo: Map + // children = [ + // {name: ["00_index"], title: "Overview"}, + // {name: ["01_getting_started"], title: "Getting Started"}, + // {name: ["01_getting_started", "01_installation"], title: "Installation"}, + // {name: ["01_getting_started", "02_hello_world"], title: "Hello World"}, + // ] + // memo = [ + // ([01_getting_started] => [1]) + // ([01_getting_started, 01_installation] => [1, 0]) + // ([01_getting_started, 02_hello_world] => [1, 0]) + // ] + // Data structure gives us ability to look up the index of a child in the parent's children array by the child's name. + // TODO: Remove name suffix starting with prefix 00. + return children; } // deno run -A server/docs/docs.ts // if (import.meta.main) { - // TODO: Rename to getFSItems. - const toc = await getTableOfContents(); - // console.log(toc); - // TODO: Rename to getTableOfContents. - const toc2 = makeTableOfContents(toc.children); - // console.log(toc2); -} - -/* - // We now have a hashmap of "path/to/a/section" => [section_entry] - // Where a section_entry is an object with it's data: {name: ["path", "to", "section"], title: "Section Title", Optional[children]: [], Optional[href]: "https://example.com"} - - // Now loop through our entries by depth - const paths = Object.keys(sections); - paths.sort((b, a) => a.split("/").length - b.split("/").length); - for (let i = 0; i < paths.length; i++) { - // Save the root path for last - if (paths[i] === "") continue; - // Get what our parent's "name" field would be - const parent_name_field = paths[i].split("/"); - // Get where our parent's path would be stored - let parent_path = ""; - if (parent_name_field.length !== 1) { - parent_path = parent_name_field.slice(0, -1).join("/"); - } - // Search for our parent - for (let j = 0; j < sections[parent_path].length; j++) { - // If their field matches what would be our field - if ( - sections[parent_path][j].name.join("/") == parent_name_field.join("/") - ) { - // Add our children to our parent - sections[parent_path][j].children = sections[paths[i]]; - // Remove our children from the list of sections - delete sections[paths[i]]; - break; - } else { - } - } - } - - // Finally, re_add the "00_index" to children at root - for (let i = 0; i < sections[""].length; i++) { - const name_field = sections[""][i].name; - if (name_field[0] === "00_index" || name_field[0].slice(0, 2) == "99") { - continue; - } else { - name_field.push("00_index"); - } - } - - // Sort titles - const ret = { children: sections[""] }; - dfs_sort(ret); - return ret; + const items = await readFSItems({ + root: ["server", "docs"], + isIndex: (suffix) => suffix.startsWith("00_"), + }); + // const parentRelationships = parentRelationshipsOf(items); + const toc = tocOf(items); + console.log({ toc }); } -*/ diff --git a/wip.ts b/wip.ts deleted file mode 100644 index a03729e..0000000 --- a/wip.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getTableOfContents } from "#/server/docs/mod.ts"; - -// deno run -A wip.ts -// -if (import.meta.main) { - const toc = await getTableOfContents(); - console.log({ toc }); -} From c8544aac7c15a31da6795ed0e9d16336370597a2 Mon Sep 17 00:00:00 2001 From: EthanThatOneKid <31261035+EthanThatOneKid@users.noreply.github.com> Date: Thu, 21 Mar 2024 12:12:34 -0700 Subject: [PATCH 08/34] wip --- server/docs/docs.ts | 14 ---- server/docs/fs.ts | 183 ++++++++++++++++++++++++++++++++++++++++++ server/docs/sample.ts | 30 +++++++ 3 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 server/docs/fs.ts create mode 100644 server/docs/sample.ts diff --git a/server/docs/docs.ts b/server/docs/docs.ts index 70d829a..065ae93 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -75,20 +75,6 @@ export async function readFSItems( return items; } -// function adjacenciesOf(items: FSItem[]): Map { -// const adjacencies = new Map(); -// for (const item of items) { -// const parent = item.name.slice(0, -1); -// if (!adjacencies.has(parent)) { -// adjacencies.set(parent, []); -// } - -// adjacencies.get(parent)!.push(item.name[item.name.length - 1]); -// } - -// return adjacencies; -// } - const ROOT_PARENT = "-"; const NAME_SEPARATOR = "/"; diff --git a/server/docs/fs.ts b/server/docs/fs.ts new file mode 100644 index 0000000..4745ccf --- /dev/null +++ b/server/docs/fs.ts @@ -0,0 +1,183 @@ +import { extract } from "@std/front-matter/any"; +import { test } from "@std/front-matter/test"; +import { walk } from "@std/fs/walk"; +import { join, parse, SEPARATOR_PATTERN } from "@std/path"; + +/** + * FSItem is an item represented in the file system. + */ +export interface FSItem { + name: string[]; + title?: string; + href?: string; +} + +/** + * RenderFSItemsOptions represents the options for rendering file-based items. + */ +export interface ReadFSItemsOptions { + root: string[]; + isIndex?: (suffix: string) => boolean; +} + +/** + * readFSItems reads the file-based items recursively. + */ +export async function readFSItems( + options: ReadFSItemsOptions, +): Promise { + const items: FSItem[] = []; + const walkIt = walk( + join(...options.root), + { exts: [".md"], includeDirs: false }, + ); + for await (const file of walkIt) { + const md = await Deno.readTextFile(file.path); + const path = parse(file.path); + // Remove index suffix from the name. + const name = options.isIndex?.(path.name) ? [] : [path.name]; + + // If the path has a directory, add it to the name. + if (path.dir !== "") { + // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556 + const parents = path.dir + .split(SEPARATOR_PATTERN) + .slice(options.root.length); + name.unshift(...parents); + } + + const item = renderFSItem(name, md); + items.push(item); + } + + return items; +} + +/** + * renderFSItem renders the FSItem. + */ +export function renderFSItem(name: string[], md: string): FSItem { + let title: string | undefined; + let href: string | undefined; + if (test(md)) { + const extracted = extract<{ title: string; href: string }>(md); + if (extracted.attrs.title !== undefined) { + title = extracted.attrs.title; + } + + if (extracted.attrs.href !== undefined) { + href = extracted.attrs.href; + } + } + + return { name, title, href }; +} + +const ROOT_PARENT = "-"; +const NAME_SEPARATOR = "/"; + +function directoriesOf(items: FSItem[]): Map { + const relationships = new Map(); + for (const item of items) { + const parentKey = + (item.name.length > 1 ? item.name.slice(0, -1) : [ROOT_PARENT]) + .join(NAME_SEPARATOR); + if (!relationships.has(parentKey)) { + relationships.set(parentKey, []); + } + + relationships.get(parentKey)!.push(item.name); + } + + return relationships; +} + +/** + * Node is a node in a tree. + */ +export type Node = + & T + & { children?: Node[] }; + +// function sortChildren(node: Node, fn: (node: Node) => number): void { +// if (node.children !== undefined) { +// node.children.sort(fn); +// node.children.forEach((child) => sortChildren(child, fn)); +// } +// } + +/** + * addItem recursively adds an item to the result. + */ +function addItem( + result: Node[], + items: FSItem[], + index: number, + depth = 0, +): void { + const item = items[index]; + const current = item.name.at(depth); + if (current === undefined) { + return; + } + + let parent = result.find((node) => node.name.at(depth) === current); + if (parent === undefined) { + parent = { ...item, children: [] }; + result.push(parent); + } + + if (parent !== undefined) { + parent.children ??= []; + } +} + +function tocOf(input: FSItem[]): Node[] { + // Restructure the input in terms of directories. + const directories = directoriesOf(input); + + // Add the item to the table of contents. + + // Construct the table of contents. + const result: Node[] = []; + const visited = new Set(); + const queue = [ROOT_PARENT]; + while (queue.length > 0) { + const parent = queue.shift()!; + if (visited.has(parent)) { + continue; + } + + const children = directories.get(parent); + if (!children) { + throw new Error(`no children for ${parent}`); + } + + for (const child of children) { + const item = input.find((item) => + item.name.join(NAME_SEPARATOR) === child.join(NAME_SEPARATOR) + ); + if (!item) { + throw new Error(`no item for ${child}`); + } + + // Write item to result recursively. + addItem(item); + } + + visited.add(parent); + } + + return result; +} + +// deno run -A server/docs/fs.ts +// +if (import.meta.main) { + const items = await readFSItems({ + root: ["server", "docs"], + isIndex: (suffix) => suffix.startsWith("00_"), + }); + const toc = tocOf(items); + console.log({ toc }); +} diff --git a/server/docs/sample.ts b/server/docs/sample.ts new file mode 100644 index 0000000..6968d5c --- /dev/null +++ b/server/docs/sample.ts @@ -0,0 +1,30 @@ +const from = [ + { name: [], title: "Overview" }, + { + name: ["01_getting_started"], + title: "Getting Started", + }, + { name: ["01_getting_started", "01_install"], title: "Install" }, + { name: ["01_getting_started", "02_use"], title: "Use" }, + { + name: ["99_view_on_github"], + title: "View on GitHub", + href: "https://github.com/FartLabs/jsonx", + }, +]; + +const to = [ + { name: [], title: "Overview" }, + { + name: ["01_getting_started"], + children: [ + { name: ["01_getting_started", "01_install"], title: "Install" }, + { name: ["01_getting_started", "02_use"], title: "Use" }, + ], + }, + { + name: ["99_view_on_github"], + title: "View on GitHub", + href: "https://github.com/FartLabs/jsonx", + }, +]; From 957f2e95d7b8b70a658b11ce3e4f3abcd1037070 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Fri, 22 Mar 2024 00:33:07 -0700 Subject: [PATCH 09/34] wip --- server/docs/docs.ts | 186 ++----------------------------------------- server/docs/items.ts | 82 +++++++++++++++++++ server/docs/tree.ts | 106 ++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 178 deletions(-) create mode 100644 server/docs/items.ts create mode 100644 server/docs/tree.ts diff --git a/server/docs/docs.ts b/server/docs/docs.ts index 70d829a..211e4a5 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -1,188 +1,18 @@ -import { extract } from "@std/front-matter/any"; -import { test } from "@std/front-matter/test"; -import type { RenderOptions } from "@deno/gfm"; -import { render } from "@deno/gfm"; -import { walk } from "@std/fs/walk"; -import { join, parse, SEPARATOR_PATTERN } from "@std/path"; +// import type { RenderOptions } from "@deno/gfm"; +// import { render } from "@deno/gfm"; +import { toTree } from "./tree.ts"; +import { readFSItems } from "./items.ts"; -/** - * renderFSItem renders the FSItem. - */ -export function renderFSItem(name: string[], md: string): FSItem { - let title: string | undefined; - let href: string | undefined; - if (test(md)) { - const extracted = extract<{ title: string; href: string }>(md); - if (extracted.attrs.title !== undefined) { - title = extracted.attrs.title; - } - - if (extracted.attrs.href !== undefined) { - href = extracted.attrs.href; - } - } - - return { name, title, href }; -} - -/** - * FSItem is an item represented in the file system. - */ -export interface FSItem { - name: string[]; - title?: string; - href?: string; -} - -/** - * RenderFSItemsOptions represents the options for rendering file-based items. - */ -export interface ReadFSItemsOptions { - root: string[]; - isIndex?: (suffix: string) => boolean; -} - -/** - * readFSItems reads the file-based items recursively. - */ -export async function readFSItems( - options: ReadFSItemsOptions, -): Promise { - const items: FSItem[] = []; - const walkIt = walk( - join(...options.root), - { exts: [".md"], includeDirs: false }, - ); - for await (const file of walkIt) { - const md = await Deno.readTextFile(file.path); - const path = parse(file.path); - // Remove index suffix from the name. - const name = options.isIndex?.(path.name) ? [] : [path.name]; - - // If the path has a directory, add it to the name. - if (path.dir !== "") { - // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556 - const parents = path.dir - .split(SEPARATOR_PATTERN) - .slice(options.root.length); - name.unshift(...parents); - } - - const item = renderFSItem(name, md); - items.push(item); - } - - return items; -} - -// function adjacenciesOf(items: FSItem[]): Map { -// const adjacencies = new Map(); -// for (const item of items) { -// const parent = item.name.slice(0, -1); -// if (!adjacencies.has(parent)) { -// adjacencies.set(parent, []); -// } - -// adjacencies.get(parent)!.push(item.name[item.name.length - 1]); -// } - -// return adjacencies; -// } - -const ROOT_PARENT = "-"; -const NAME_SEPARATOR = "/"; - -function parentRelationshipsOf(items: FSItem[]): Map { - const relationships = new Map(); - for (const item of items) { - const parentKey = - (item.name.length > 1 ? item.name.slice(0, -1) : [ROOT_PARENT]) - .join(NAME_SEPARATOR); - if (!relationships.has(parentKey)) { - relationships.set(parentKey, []); - } - - relationships.get(parentKey)!.push(item.name); - } - - return relationships; -} - -export type Node = - & T - & { children?: Node[] }; - -// function sortChildren(node: Node, fn: (node: Node) => number): void { -// if (node.children !== undefined) { -// node.children.sort(fn); -// node.children.forEach((child) => sortChildren(child, fn)); -// } -// } - -function tocOf(input: FSItem[]): Node[] { - // Get the parent relationships of the input. - const parentRelationships = parentRelationshipsOf(input); - - // Construct the table of contents. - const children: Node[] = []; - - // function appendChildren(name: string[]) { - // // Add the item to the table of contents. - // const key = name.join(NAME_SEPARATOR); - // const item = input.find((item) => item.name.join(NAME_SEPARATOR) === key); - // let node: Node= - // while (node.children !== undefined) { - // } - - // // if (parentRelationships.has(key)) { - // // } - // } - - // appendChildren([ROOT_PARENT]); - console.log({ children }); - - // const memo = new Map(); - // while (true) { - // for (const child of children) { - // if (!memo.has(child.name)) { - // memo.set(child.name, []); - // } - - // Iterate over top level of children. - // For each child, check if it has a parent. - // children.forEach((child) => { - // if (memo.has(child.name)) { - // If a child has a parent, add it to the parent's children array. - - // If a child has no parent, add it to the top level of the table of contents. - // } - // - // - //// memo: Map - // children = [ - // {name: ["00_index"], title: "Overview"}, - // {name: ["01_getting_started"], title: "Getting Started"}, - // {name: ["01_getting_started", "01_installation"], title: "Installation"}, - // {name: ["01_getting_started", "02_hello_world"], title: "Hello World"}, - // ] - // memo = [ - // ([01_getting_started] => [1]) - // ([01_getting_started, 01_installation] => [1, 0]) - // ([01_getting_started, 02_hello_world] => [1, 0]) - // ] - // Data structure gives us ability to look up the index of a child in the parent's children array by the child's name. - // TODO: Remove name suffix starting with prefix 00. - return children; -} +// TODO: Render docs. // deno run -A server/docs/docs.ts // if (import.meta.main) { const items = await readFSItems({ root: ["server", "docs"], + namespace: "-", isIndex: (suffix) => suffix.startsWith("00_"), }); - // const parentRelationships = parentRelationshipsOf(items); - const toc = tocOf(items); - console.log({ toc }); + const tree = toTree(items); + console.dir({ tree }, { depth: null }); } diff --git a/server/docs/items.ts b/server/docs/items.ts new file mode 100644 index 0000000..66b7ede --- /dev/null +++ b/server/docs/items.ts @@ -0,0 +1,82 @@ +import { extract } from "@std/front-matter/any"; +import { test } from "@std/front-matter/test"; +import { walk } from "@std/fs/walk"; +import { join, parse, SEPARATOR_PATTERN } from "@std/path"; + +/** + * FSItem is an item represented in the file system. + */ +export interface FSItem { + name: string[]; + title?: string; + href?: string; +} + +/** + * RenderFSItemsOptions represents the options for rendering file-based items. + */ +export interface ReadFSItemsOptions { + root: string[]; + namespace?: string; + isIndex?: (suffix: string) => boolean; +} + +/** + * readFSItems reads the file-based items recursively. + */ +export async function readFSItems( + options: ReadFSItemsOptions, +): Promise { + const items: FSItem[] = []; + const walkIt = walk( + join(...options.root), + { exts: [".md"], includeDirs: false }, + ); + for await (const file of walkIt) { + const md = await Deno.readTextFile(file.path); + const path = parse(file.path); + + // Remove index suffix from the name. + const name = options.isIndex?.(path.name) ? [] : [path.name]; + + // If the path has a directory, add it to the name. + if (path.dir !== "") { + // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556 + const parents = path.dir + .split(SEPARATOR_PATTERN) + .slice(options.root.length); + name.unshift(...parents); + } + + // Add namespace to the name. + if (options.namespace !== undefined) { + name.unshift(options.namespace); + } + + // Render the FSItem. + const item = renderFSItem(name, md); + items.push(item); + } + + return items; +} + +/** + * renderFSItem renders the FSItem. + */ +export function renderFSItem(name: string[], md: string): FSItem { + let title: string | undefined; + let href: string | undefined; + if (test(md)) { + const extracted = extract<{ title: string; href: string }>(md); + if (extracted.attrs.title !== undefined) { + title = extracted.attrs.title; + } + + if (extracted.attrs.href !== undefined) { + href = extracted.attrs.href; + } + } + + return { name, title, href }; +} diff --git a/server/docs/tree.ts b/server/docs/tree.ts new file mode 100644 index 0000000..3f09892 --- /dev/null +++ b/server/docs/tree.ts @@ -0,0 +1,106 @@ +const from = [ + { name: [], title: "Overview", href: undefined }, + { + name: ["01_getting_started"], + title: "Getting Started", + href: undefined, + }, + { + name: ["01_getting_started", "01_install"], + title: "Install", + href: undefined, + }, + { + name: ["01_getting_started", "02_use"], + title: "Use", + href: undefined, + }, + { + name: ["99_view_on_github"], + title: "View on GitHub", + href: "https://github.com/FartLabs/jsonx", + }, +].sort(() => Math.random() - 0.5); + +const to = [ + { name: [], title: "Overview", href: undefined }, + { + name: ["01_getting_started"], + title: "Getting Started", + href: undefined, + children: [ + { + name: ["01_getting_started", "01_install"], + title: "Install", + href: undefined, + }, + { + name: ["01_getting_started", "02_use"], + title: "Use", + href: undefined, + }, + ], + }, + { + name: ["99_view_on_github"], + title: "View on GitHub", + href: "https://github.com/FartLabs/jsonx", + }, +]; + +/** + * FSItem is an item represented in the file system. + */ +export interface FSItem { + name: string[]; + title?: string; + href?: string; +} + +/** + * Node is a node in a tree. + */ +export type Node = + & T + & { children?: Node[] }; + +/** + * toTree converts an array of FSItems to a tree. + */ +export function toTree(items: FSItem[]): Node[] { + const root: Node = { name: [] }; + for (const item of items) { + let node = root; + for (const part of item.name) { + let child = node.children?.find((child) => child.name[0] === part); + if (child === undefined) { + if (node.children === undefined) { + node.children = []; + } + + child = { name: [part] }; + node.children.push(child); + } + + node = child; + } + node.title = item.title; + node.href = item.href; + } + + sortChildren( + root, + (a, b) => a.name.join("/").localeCompare(b.name.join("/")), + ); + return root.children ?? []; +} + +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)); + } +} From 50b24b161ab3e7c2e5ff6384a5625e8643225d94 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Fri, 22 Mar 2024 00:36:34 -0700 Subject: [PATCH 10/34] wip --- server/docs/docs.ts | 1 - server/docs/tree.ts | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/docs/docs.ts b/server/docs/docs.ts index 211e4a5..5bb5568 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -10,7 +10,6 @@ import { readFSItems } from "./items.ts"; if (import.meta.main) { const items = await readFSItems({ root: ["server", "docs"], - namespace: "-", isIndex: (suffix) => suffix.startsWith("00_"), }); const tree = toTree(items); diff --git a/server/docs/tree.ts b/server/docs/tree.ts index 3f09892..b99d7e0 100644 --- a/server/docs/tree.ts +++ b/server/docs/tree.ts @@ -71,14 +71,15 @@ export function toTree(items: FSItem[]): Node[] { const root: Node = { name: [] }; for (const item of items) { let node = root; - for (const part of item.name) { + for (let i = 0; i < item.name.length; i++) { + const part = item.name[i]; let child = node.children?.find((child) => child.name[0] === part); if (child === undefined) { if (node.children === undefined) { node.children = []; } - child = { name: [part] }; + child = { name: item.name.slice(0, i + 1) }; node.children.push(child); } From 3ccc6065f2a9829a3eb9467d622d8df87e566e55 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:49:32 -0700 Subject: [PATCH 11/34] wip --- .../01_getting_started/03_foo/00_index.md | 5 ++ .../docs/01_getting_started/03_foo/01_bar.md | 5 ++ server/docs/docs.ts | 5 +- server/docs/items.ts | 87 +++++++++++-------- server/docs/tree.ts | 77 +++++----------- 5 files changed, 89 insertions(+), 90 deletions(-) create mode 100644 server/docs/01_getting_started/03_foo/00_index.md create mode 100644 server/docs/01_getting_started/03_foo/01_bar.md diff --git a/server/docs/01_getting_started/03_foo/00_index.md b/server/docs/01_getting_started/03_foo/00_index.md new file mode 100644 index 0000000..070cd06 --- /dev/null +++ b/server/docs/01_getting_started/03_foo/00_index.md @@ -0,0 +1,5 @@ +--- +title: Foo +--- + +# Foo diff --git a/server/docs/01_getting_started/03_foo/01_bar.md b/server/docs/01_getting_started/03_foo/01_bar.md new file mode 100644 index 0000000..6dff76e --- /dev/null +++ b/server/docs/01_getting_started/03_foo/01_bar.md @@ -0,0 +1,5 @@ +--- +title: Bar +--- + +# Bar diff --git a/server/docs/docs.ts b/server/docs/docs.ts index 5bb5568..8b7bde3 100644 --- a/server/docs/docs.ts +++ b/server/docs/docs.ts @@ -8,8 +8,9 @@ import { readFSItems } from "./items.ts"; // deno run -A server/docs/docs.ts // if (import.meta.main) { - const items = await readFSItems({ - root: ["server", "docs"], + const { items } = await readFSItems({ + // root: ["server", "docs"], + root: new URL(".", import.meta.url), isIndex: (suffix) => suffix.startsWith("00_"), }); const tree = toTree(items); diff --git a/server/docs/items.ts b/server/docs/items.ts index 66b7ede..575171c 100644 --- a/server/docs/items.ts +++ b/server/docs/items.ts @@ -1,7 +1,8 @@ +import { render, type RenderOptions } from "@deno/gfm"; import { extract } from "@std/front-matter/any"; import { test } from "@std/front-matter/test"; import { walk } from "@std/fs/walk"; -import { join, parse, SEPARATOR_PATTERN } from "@std/path"; +import { dirname, parse, SEPARATOR_PATTERN, toFileUrl } from "@std/path"; /** * FSItem is an item represented in the file system. @@ -16,9 +17,25 @@ export interface FSItem { * RenderFSItemsOptions represents the options for rendering file-based items. */ export interface ReadFSItemsOptions { - root: string[]; - namespace?: string; + root: string | URL; isIndex?: (suffix: string) => boolean; + renderOptions?: RenderOptions; +} + +/** + * Content represents the content of an item. + */ +export interface Content { + md: string; + html: string; +} + +/** + * ReadFSItemsResult represents the result of reading file-based items. + */ +export interface ReadFSItemsResult { + items: FSItem[]; + contents: Map; } /** @@ -26,57 +43,59 @@ export interface ReadFSItemsOptions { */ export async function readFSItems( options: ReadFSItemsOptions, -): Promise { +): Promise { const items: FSItem[] = []; + const contents = new Map(); const walkIt = walk( - join(...options.root), + options.root, { exts: [".md"], includeDirs: false }, ); for await (const file of walkIt) { - const md = await Deno.readTextFile(file.path); + let md = await Deno.readTextFile(file.path); + const html = render(md, options.renderOptions); const path = parse(file.path); // Remove index suffix from the name. const name = options.isIndex?.(path.name) ? [] : [path.name]; + console.dir({ + file, + path, + dir1: toFileUrl(path.dir).toString(), + dir2: dirname(options.root.toString()), + }, { depth: null }); + // If the path has a directory, add it to the name. if (path.dir !== "") { // https://discord.com/channels/684898665143206084/684898665151594506/1217030758686785556 - const parents = path.dir - .split(SEPARATOR_PATTERN) - .slice(options.root.length); - name.unshift(...parents); - } - - // Add namespace to the name. - if (options.namespace !== undefined) { - name.unshift(options.namespace); + const parent = path.dir + // .slice(dirname(options.root.toString()).length) + .split(SEPARATOR_PATTERN); + name.unshift(...parent); } // Render the FSItem. - const item = renderFSItem(name, md); - items.push(item); - } + let title: string | undefined; + let href: string | undefined; + if (test(md)) { + const extracted = extract<{ title: string; href: string }>(md); + if (extracted.attrs.title !== undefined) { + title = extracted.attrs.title; + } - return items; -} + if (extracted.attrs.href !== undefined) { + href = extracted.attrs.href; + } -/** - * renderFSItem renders the FSItem. - */ -export function renderFSItem(name: string[], md: string): FSItem { - let title: string | undefined; - let href: string | undefined; - if (test(md)) { - const extracted = extract<{ title: string; href: string }>(md); - if (extracted.attrs.title !== undefined) { - title = extracted.attrs.title; + md = extracted.body; } - if (extracted.attrs.href !== undefined) { - href = extracted.attrs.href; - } + const item = { name, title, href }; + items.push(item); + + // Store the item contents. + contents.set(file.path, { md, html }); } - return { name, title, href }; + return { items, contents }; } diff --git a/server/docs/tree.ts b/server/docs/tree.ts index b99d7e0..65884b4 100644 --- a/server/docs/tree.ts +++ b/server/docs/tree.ts @@ -1,53 +1,3 @@ -const from = [ - { name: [], title: "Overview", href: undefined }, - { - name: ["01_getting_started"], - title: "Getting Started", - href: undefined, - }, - { - name: ["01_getting_started", "01_install"], - title: "Install", - href: undefined, - }, - { - name: ["01_getting_started", "02_use"], - title: "Use", - href: undefined, - }, - { - name: ["99_view_on_github"], - title: "View on GitHub", - href: "https://github.com/FartLabs/jsonx", - }, -].sort(() => Math.random() - 0.5); - -const to = [ - { name: [], title: "Overview", href: undefined }, - { - name: ["01_getting_started"], - title: "Getting Started", - href: undefined, - children: [ - { - name: ["01_getting_started", "01_install"], - title: "Install", - href: undefined, - }, - { - name: ["01_getting_started", "02_use"], - title: "Use", - href: undefined, - }, - ], - }, - { - name: ["99_view_on_github"], - title: "View on GitHub", - href: "https://github.com/FartLabs/jsonx", - }, -]; - /** * FSItem is an item represented in the file system. */ @@ -64,16 +14,28 @@ export type Node = & T & { children?: Node[] }; +/** + * NAME_SEPARATOR is the separator for an FSItem name. + */ +export const NAME_SEPARATOR = "/"; + /** * toTree converts an array of FSItems to a tree. */ export function toTree(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[i]; - let child = node.children?.find((child) => child.name[0] === part); + 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 = []; @@ -91,9 +53,16 @@ export function toTree(items: FSItem[]): Node[] { sortChildren( root, - (a, b) => a.name.join("/").localeCompare(b.name.join("/")), + (a, b) => + a.name.join(NAME_SEPARATOR).localeCompare(b.name.join(NAME_SEPARATOR)), ); - return root.children ?? []; + + const children = root.children ?? []; + if (visitedRoot) { + children.unshift({ name: [], title: root.title, href: root.href }); + } + + return children; } function sortChildren( From 5f69bcde07417a5a799760fd0518597129348f93 Mon Sep 17 00:00:00 2001 From: EthanThatOneKid <31261035+EthanThatOneKid@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:18:08 -0700 Subject: [PATCH 12/34] file `server/docs/docs.ts` is coming together --- client/components/nav.tsx | 28 --- components/nav.tsx | 24 ++ .../playground/playground.tsx | 0 .../playground/scripts.tsx | 0 components/toc.tsx | 49 ++++ routes/api/playgrounds/[id].ts | 7 +- routes/api/playgrounds/index.ts | 4 +- routes/index.tsx | 12 +- routes/playgrounds/[id].tsx | 8 +- server/docs/98_api_docs.md | 4 + server/docs/docs.ts | 22 +- server/docs/fs.ts | 224 ++++++++---------- server/docs/items.ts | 93 -------- server/docs/mod.ts | 1 - server/docs/nodes.ts | 19 ++ server/docs/sample.ts | 30 --- server/docs/tree.ts | 76 ------ server/{ => kv}/kv.ts | 0 server/{ => kv}/playgrounds.ts | 0 19 files changed, 215 insertions(+), 386 deletions(-) delete mode 100644 client/components/nav.tsx create mode 100644 components/nav.tsx rename {client/components => components}/playground/playground.tsx (100%) rename {client/components => components}/playground/scripts.tsx (100%) create mode 100644 components/toc.tsx create mode 100644 server/docs/98_api_docs.md delete mode 100644 server/docs/mod.ts create mode 100644 server/docs/nodes.ts delete mode 100644 server/docs/sample.ts delete mode 100644 server/docs/tree.ts rename server/{ => kv}/kv.ts (100%) rename server/{ => kv}/playgrounds.ts (100%) diff --git a/client/components/nav.tsx b/client/components/nav.tsx deleted file mode 100644 index 7afc393..0000000 --- a/client/components/nav.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export default function Nav() { - return ( - - ); -} diff --git a/components/nav.tsx b/components/nav.tsx new file mode 100644 index 0000000..931e222 --- /dev/null +++ b/components/nav.tsx @@ -0,0 +1,24 @@ +import ToC from "#/components/toc.tsx"; +import { nodes } from "#/server/docs/docs.ts"; + +/** + * NavProps are the properties for the Nav component. + */ +export interface NavProps { + path: string[]; +} + +/** + * Nav is the navigation bar for the documentation. + */ +export default function Nav(props: NavProps) { + return ( + + ); +} diff --git a/client/components/playground/playground.tsx b/components/playground/playground.tsx similarity index 100% rename from client/components/playground/playground.tsx rename to components/playground/playground.tsx diff --git a/client/components/playground/scripts.tsx b/components/playground/scripts.tsx similarity index 100% rename from client/components/playground/scripts.tsx rename to components/playground/scripts.tsx diff --git a/components/toc.tsx b/components/toc.tsx new file mode 100644 index 0000000..4a78cf9 --- /dev/null +++ b/components/toc.tsx @@ -0,0 +1,49 @@ +import type { Node } from "#/server/docs/nodes.ts"; +import type { FSItem } from "#/server/docs/items.ts"; + +/** + * ToCProps are the properties for the ToC component. + */ +export interface ToCProps { + path: string[]; + nodes: Node[]; +} + +/** + * ToC is a recursive component that renders a table of contents. + */ +export default function ToC(props: ToCProps) { + return ( +
    + {props.nodes.map((node) => { + return ( +
  • + {node.title && ( + {node.title} + )} + {node.children && startsWith(props.path, node.name) && ( + + )} +
  • + ); + })} +
+ ); +} + +function startsWith(path: string[], prefix: string[]): boolean { + if (path.length < prefix.length) { + return false; + } + + for (let i = 0; i < prefix.length; i++) { + if (path[i] !== prefix[i]) { + return false; + } + } + + return true; +} diff --git a/routes/api/playgrounds/[id].ts b/routes/api/playgrounds/[id].ts index e71e8e6..58ca265 100644 --- a/routes/api/playgrounds/[id].ts +++ b/routes/api/playgrounds/[id].ts @@ -1,6 +1,9 @@ import type { Handlers } from "$fresh/server.ts"; -import { getPlayground, setPlayground } from "#/server/playgrounds.ts"; -import { kv } from "#/server/kv.ts"; +import { + getPlayground, + setPlayground, +} from "../../../server/kv/playgrounds.ts"; +import { kv } from "#/server/kv/kv.ts"; export const handler: Handlers = { async GET(_request, ctx) { diff --git a/routes/api/playgrounds/index.ts b/routes/api/playgrounds/index.ts index 4738796..94751ef 100644 --- a/routes/api/playgrounds/index.ts +++ b/routes/api/playgrounds/index.ts @@ -1,7 +1,7 @@ import type { Handlers } from "$fresh/server.ts"; import type { AddPlaygroundRequest } from "#/client/playgrounds.ts"; -import { addPlayground } from "#/server/playgrounds.ts"; -import { kv } from "#/server/kv.ts"; +import { addPlayground } from "#/server/kv/playgrounds.ts"; +import { kv } from "#/server/kv/kv.ts"; export const handler: Handlers = { async POST(request, _ctx) { diff --git a/routes/index.tsx b/routes/index.tsx index ca07b6e..3196172 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -1,10 +1,10 @@ import { Head } from "$fresh/runtime.ts"; -import Playground from "#/client/components/playground/playground.tsx"; -import Nav from "#/client/components/nav.tsx"; +import Playground from "#/components/playground/playground.tsx"; +import Nav from "#/components/nav.tsx"; import { getMeta } from "#/client/meta.ts"; -import { kv } from "#/server/kv.ts"; +import { kv } from "#/server/kv/kv.ts"; import { getExampleByName } from "#/server/examples/mod.ts"; -import { getPlayground } from "#/server/playgrounds.ts"; +import { getPlayground } from "#/server/kv/playgrounds.ts"; export default async function Home(request: Request) { const url = new URL(request.url); @@ -24,7 +24,7 @@ export default async function Home(request: Request) { } const meta = await getMeta(); - + const path = url.pathname.split("/").filter((part) => part !== ""); return ( <> @@ -32,7 +32,7 @@ export default async function Home(request: Request) { -