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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ It is not a serializer, not an ORM, and not a new religion. It just does this on
## Documentation

- [Docs Site](https://maxatwork.github.io/form2js/) - overview, installation, unified playground, and published API reference.
- [API Reference Source](docs/api.md) - markdown source for the published API docs page.
- [API Reference Source](docs/api-index.md) - markdown source for the published API docs page.

## Packages

Expand Down
37 changes: 37 additions & 0 deletions apps/docs/src/components/api/ApiPackageNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";

import type { ApiPackageEntry } from "../../lib/api-packages";
import { apiPackageDocsPath } from "../../lib/site-routes";

type ApiPackageNavEntry = Pick<ApiPackageEntry, "slug" | "packageName">;

interface ApiPackageNavProps {
activeSlug?: ApiPackageNavEntry["slug"];
basePath: string;
packages: ApiPackageNavEntry[];
}

export function ApiPackageNav({
activeSlug,
basePath,
packages
}: ApiPackageNavProps): React.JSX.Element {
return (
<nav aria-label="API packages" className="api-package-nav">
<p className="api-package-nav__eyebrow">Packages</p>
<ul className="api-package-nav__list">
{packages.map((entry) => (
<li key={entry.slug}>
<a
aria-current={entry.slug === activeSlug ? "page" : undefined}
className="api-package-nav__link"
href={apiPackageDocsPath(basePath, entry.slug)}
>
{entry.packageName}
</a>
</li>
))}
</ul>
</nav>
);
}
35 changes: 35 additions & 0 deletions apps/docs/src/components/api/ApiPackageSummaryList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react";

import type { ApiPackageEntry } from "../../lib/api-packages";
import { apiPackageDocsPath } from "../../lib/site-routes";

type ApiPackageSummaryEntry = Pick<
ApiPackageEntry,
"slug" | "packageName" | "summary"
>;

interface ApiPackageSummaryListProps {
basePath: string;
packages: ApiPackageSummaryEntry[];
}

export function ApiPackageSummaryList({
basePath,
packages
}: ApiPackageSummaryListProps): React.JSX.Element {
return (
<section aria-labelledby="api-package-list" className="api-package-summary-list">
<h2 id="api-package-list">Packages</h2>
<div className="api-package-summary-list__items">
{packages.map((entry) => (
<article className="api-package-summary" key={entry.slug}>
<h3>
<a href={apiPackageDocsPath(basePath, entry.slug)}>{entry.packageName}</a>
</h3>
<p>{entry.summary}</p>
</article>
))}
</div>
</section>
);
}
24 changes: 20 additions & 4 deletions apps/docs/src/layouts/ApiDocsLayout.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
import { ApiPackageNav } from "../components/api/ApiPackageNav";
import { ApiToc } from "../components/api/ApiToc";
import type { ApiPackageEntry } from "../lib/api-packages";
import type { ApiHeading } from "../lib/api-docs-source";
import DocsShell from "./DocsShell.astro";

Expand All @@ -8,23 +10,37 @@ interface Props {
introHtml: string;
bodyHtml: string;
headings: ApiHeading[];
packages: ApiPackageEntry[];
activePackageSlug?: ApiPackageEntry["slug"];
}

const { title, introHtml, bodyHtml, headings } = Astro.props;
const { title, introHtml, bodyHtml, headings, packages, activePackageSlug } =
Astro.props;
const basePath = import.meta.env.BASE_URL;
---

<DocsShell title={`${title} | form2js`}>
<section class="api-docs">
<aside class="api-docs__nav">
<ApiPackageNav
activeSlug={activePackageSlug}
basePath={basePath}
packages={packages}
/>
</aside>
<article class="api-docs__content">
<header class="api-docs__header">
<p class="hero__eyebrow">API Docs</p>
<h1>{title}</h1>
{introHtml ? <div class="api-docs__intro" set:html={introHtml} /> : null}
</header>
<slot name="summary" />
<div class="api-docs__body" set:html={bodyHtml} />
</article>
<aside class="api-docs__sidebar">
<ApiToc client:load headings={headings} />
</aside>
{headings.length > 0 ? (
<aside class="api-docs__sidebar">
<ApiToc client:load headings={headings} />
</aside>
) : null}
</section>
</DocsShell>
31 changes: 17 additions & 14 deletions apps/docs/src/lib/api-docs-source.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import rehypeStringify from "rehype-stringify";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";

import { apiDocsPath, homepagePath } from "./site-routes";
import {
apiIndexMarkdownPath,
getApiPackageByMarkdownBasename
} from "./api-packages";
import { apiDocsPath, apiPackageDocsPath, homepagePath } from "./site-routes";

export interface ApiHeading {
depth: 2 | 3;
Expand Down Expand Up @@ -63,24 +66,26 @@ function rewriteMarkdownLink(url: string, basePath: string): string {

const [pathname, hash] = url.split("#");
const suffix = hash ? `#${hash}` : "";
const normalizedPathname = pathname
.replace(/^(?:\.\.\/)?docs\//, "")
.replace(/^\.\//, "");

if (
pathname === "README.md" ||
pathname === "./README.md" ||
normalizedPathname === "README.md" ||
pathname === "../README.md"
) {
return `${homepagePath(basePath)}${suffix}`;
}

if (
pathname === "api.md" ||
pathname === "./api.md" ||
pathname === "docs/api.md" ||
pathname === "../docs/api.md"
) {
if (normalizedPathname === "api.md" || normalizedPathname === "api-index.md") {
return `${apiDocsPath(basePath)}${suffix}`;
}

const apiPackage = getApiPackageByMarkdownBasename(normalizedPathname);
if (apiPackage) {
return `${apiPackageDocsPath(basePath, apiPackage.slug)}${suffix}`;
}

return url;
}

Expand Down Expand Up @@ -135,7 +140,7 @@ export function parseApiDocsMarkdown(
const titleNode = rootChildren[0];

if (titleNode?.type !== "heading" || titleNode.depth !== 1) {
throw new Error("docs/api.md must start with an H1 heading.");
throw new Error("API docs markdown must start with an H1 heading.");
}

const title = collectText(titleNode);
Expand Down Expand Up @@ -217,9 +222,7 @@ export function parseApiDocsMarkdown(
export async function loadApiDocsSource(
options: { basePath?: string; markdownPath?: string } = {}
): Promise<ApiDocsSource> {
const markdownPath =
options.markdownPath ??
path.resolve(process.cwd(), "..", "..", "docs", "api.md");
const markdownPath = options.markdownPath ?? apiIndexMarkdownPath;
const markdown = await readFile(markdownPath, "utf8");

return parseApiDocsMarkdown(markdown, {
Expand Down
73 changes: 73 additions & 0 deletions apps/docs/src/lib/api-packages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import path from "node:path";

export type ApiPackageSlug =
| "core"
| "dom"
| "form-data"
| "react"
| "js2form"
| "jquery";

export interface ApiPackageEntry {
slug: ApiPackageSlug;
packageName: string;
summary: string;
markdownPath: string;
}

function resolveDocsPath(filename: string): string {
return path.resolve(process.cwd(), "..", "..", "docs", filename);
}

export const apiIndexMarkdownPath = resolveDocsPath("api-index.md");

export const apiPackages: ApiPackageEntry[] = [
{
slug: "core",
packageName: "@form2js/core",
summary: "Turn path-like key/value pairs into nested objects and flatten them back into entries.",
markdownPath: resolveDocsPath("api-core.md")
},
{
slug: "dom",
packageName: "@form2js/dom",
summary: "Parse browser form controls into an object while preserving native submission behavior.",
markdownPath: resolveDocsPath("api-dom.md")
},
{
slug: "form-data",
packageName: "@form2js/form-data",
summary: "Parse FormData and tuple entries with the same path rules used by the core parser.",
markdownPath: resolveDocsPath("api-form-data.md")
},
{
slug: "react",
packageName: "@form2js/react",
summary: "Handle React form submission with parsed payloads, optional schema validation, and submit state.",
markdownPath: resolveDocsPath("api-react.md")
},
{
slug: "js2form",
packageName: "@form2js/js2form",
summary: "Push nested object data back into matching DOM form controls.",
markdownPath: resolveDocsPath("api-js2form.md")
},
{
slug: "jquery",
packageName: "@form2js/jquery",
summary: "Install a jQuery plugin wrapper around the DOM parser for legacy form handling flows.",
markdownPath: resolveDocsPath("api-jquery.md")
}
];

export function getApiPackageBySlug(slug: string): ApiPackageEntry | undefined {
return apiPackages.find((entry) => entry.slug === slug);
}

export function getApiPackageByMarkdownBasename(
basename: string
): ApiPackageEntry | undefined {
return apiPackages.find(
(entry) => path.basename(entry.markdownPath) === basename
);
}
4 changes: 4 additions & 0 deletions apps/docs/src/lib/site-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function apiDocsPath(basePath: string): string {
return `${normalizeBase(basePath)}api/`;
}

export function apiPackageDocsPath(basePath: string, slug: string): string {
return `${apiDocsPath(basePath)}${encodeURIComponent(slug)}/`;
}

export function homepageVariantPath(basePath: string, variant: string): string {
return `${normalizeBase(basePath)}?variant=${encodeURIComponent(variant)}`;
}
31 changes: 31 additions & 0 deletions apps/docs/src/pages/api/[package].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
import ApiDocsLayout from "../../layouts/ApiDocsLayout.astro";
import { apiPackages, type ApiPackageEntry } from "../../lib/api-packages";
import { loadApiDocsSource } from "../../lib/api-docs-source";

interface Props {
apiPackage: ApiPackageEntry;
}

export function getStaticPaths() {
return apiPackages.map((apiPackage) => ({
params: { package: apiPackage.slug },
props: { apiPackage }
}));
}

const { apiPackage } = Astro.props;
const apiDocsSource = await loadApiDocsSource({
basePath: import.meta.env.BASE_URL,
markdownPath: apiPackage.markdownPath
});
---

<ApiDocsLayout
activePackageSlug={apiPackage.slug}
bodyHtml={apiDocsSource.bodyHtml}
headings={apiDocsSource.headings}
introHtml={apiDocsSource.introHtml}
packages={apiPackages}
title={apiDocsSource.title}
/>
11 changes: 10 additions & 1 deletion apps/docs/src/pages/api/index.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
import { ApiPackageSummaryList } from "../../components/api/ApiPackageSummaryList";
import ApiDocsLayout from "../../layouts/ApiDocsLayout.astro";
import { apiPackages } from "../../lib/api-packages";
import { loadApiDocsSource } from "../../lib/api-docs-source";

const apiDocsSource = await loadApiDocsSource({
Expand All @@ -11,5 +13,12 @@ const apiDocsSource = await loadApiDocsSource({
bodyHtml={apiDocsSource.bodyHtml}
headings={apiDocsSource.headings}
introHtml={apiDocsSource.introHtml}
packages={apiPackages}
title={apiDocsSource.title}
/>
>
<ApiPackageSummaryList
slot="summary"
basePath={import.meta.env.BASE_URL}
packages={apiPackages}
/>
</ApiDocsLayout>
Loading
Loading