chore: import upstream snapshot with attribution
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { docs, findDocBySlug } from "./docs";
|
||||
import type { Article, Heading } from "./types";
|
||||
|
||||
const ARTICLES_ROOT = join(process.cwd(), "articles");
|
||||
|
||||
export async function readArticleBySlug(slug: string): Promise<string | null> {
|
||||
const doc = findDocBySlug(slug);
|
||||
if (!doc) return null;
|
||||
const relative = doc.sourcePath.replace(/^\/articles\//, "");
|
||||
const filePath = join(ARTICLES_ROOT, relative);
|
||||
return readFile(filePath, "utf8");
|
||||
}
|
||||
|
||||
export async function readArticleByPath(routePath: string): Promise<Article | null> {
|
||||
const doc = docs.find((d) => d.path === routePath);
|
||||
if (!doc) return null;
|
||||
const source = await readArticleBySlug(doc.slug);
|
||||
if (source === null) return null;
|
||||
return { doc, source };
|
||||
}
|
||||
|
||||
export function extractHeadings(markdown: string): Heading[] {
|
||||
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||
const headings: Heading[] = [];
|
||||
let inCodeBlock = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith("```")) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
if (inCodeBlock) continue;
|
||||
|
||||
const match = /^(#{2,4})\s+(.+)$/.exec(line.trim());
|
||||
if (match) {
|
||||
const text = match[2].replace(/`([^`]+)`/g, "$1");
|
||||
headings.push({
|
||||
level: match[1].length as Heading["level"],
|
||||
text,
|
||||
id: slugify(match[2]),
|
||||
});
|
||||
}
|
||||
}
|
||||
return headings;
|
||||
}
|
||||
|
||||
function slugify(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
import type { Doc, DocsGroup } from "./types";
|
||||
|
||||
const SECTION_ORDER = [
|
||||
"Start Here",
|
||||
"Concepts",
|
||||
"Reference",
|
||||
"Agent Workflow",
|
||||
"Language Pieces",
|
||||
"Standard Library",
|
||||
"Core",
|
||||
"Text And Data",
|
||||
"Programs",
|
||||
"Runtime And Web",
|
||||
"Build And Runtime",
|
||||
];
|
||||
|
||||
export const docs: Doc[] = [
|
||||
{
|
||||
slug: "getting-started",
|
||||
title: "Getting Started",
|
||||
description: "Install Zerolang and ask an agent to create a graph-first program you can review.",
|
||||
path: "/getting-started",
|
||||
sourcePath: "/articles/getting-started.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "install",
|
||||
title: "Install",
|
||||
description: "Install the latest compiler release and validate your environment.",
|
||||
path: "/install",
|
||||
sourcePath: "/articles/install.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "learn-zero",
|
||||
title: "Learn Zerolang",
|
||||
description: "A human tour of Zerolang's graph-first workflow, projections, and agent conversations.",
|
||||
path: "/learn",
|
||||
sourcePath: "/articles/learn-zero.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "examples",
|
||||
title: "Examples",
|
||||
description: "Runnable graph examples in learning order with copyable commands.",
|
||||
path: "/examples",
|
||||
sourcePath: "/articles/examples.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "graph-architecture",
|
||||
title: "Graph Architecture",
|
||||
description: "How Zerolang makes the semantic graph the program instead of treating text as the primary interface.",
|
||||
path: "/concepts/graph-architecture",
|
||||
sourcePath: "/articles/concepts/graph-architecture.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "semantic-vs-text",
|
||||
title: "Semantic Graph Vs Text",
|
||||
description: "How semantic graph edits differ from source-text edits, and why that matters for agents.",
|
||||
path: "/concepts/semantic-vs-text",
|
||||
sourcePath: "/articles/concepts/semantic-vs-text.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "compile-path",
|
||||
title: "Compile Path",
|
||||
description: "How Zerolang's graph-native compile path compares with traditional parse-first compilers.",
|
||||
path: "/concepts/compile-path",
|
||||
sourcePath: "/articles/concepts/compile-path.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "projections",
|
||||
title: "Projections And Round Trips",
|
||||
description: "How .0 projections support human review, manual edits, import/export, and no silent divergence.",
|
||||
path: "/concepts/projections",
|
||||
sourcePath: "/articles/concepts/projections.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "cli-reference",
|
||||
title: "CLI Reference",
|
||||
description: "Commands for creating, inspecting, patching, validating, running, and building Zero programs.",
|
||||
path: "/cli",
|
||||
sourcePath: "/articles/cli-reference.md",
|
||||
section: "Reference",
|
||||
},
|
||||
{
|
||||
slug: "diagnostics",
|
||||
title: "Diagnostics And Repair",
|
||||
description: "How humans and agents read errors, structured diagnostics, and repair plans.",
|
||||
path: "/diagnostics",
|
||||
sourcePath: "/articles/diagnostics.md",
|
||||
section: "Agent Workflow",
|
||||
},
|
||||
{
|
||||
slug: "testing",
|
||||
title: "Testing And Reliability",
|
||||
description: "Graph-backed zero test workflows, package tests, snapshots, fuzzing, and hardening gates.",
|
||||
path: "/testing",
|
||||
sourcePath: "/articles/testing.md",
|
||||
section: "Agent Workflow",
|
||||
},
|
||||
{
|
||||
slug: "primitives",
|
||||
title: "Primitives And Types",
|
||||
description: "The graph-visible language pieces behind both graph facts and projection syntax.",
|
||||
path: "/primitives",
|
||||
sourcePath: "/articles/primitives.md",
|
||||
section: "Language Pieces",
|
||||
},
|
||||
{
|
||||
slug: "language-reference",
|
||||
title: "Language Model Reference",
|
||||
description: "Declarations, functions, control flow, capabilities, ownership, packages, and projections.",
|
||||
path: "/reference",
|
||||
sourcePath: "/articles/language-reference.md",
|
||||
section: "Language Pieces",
|
||||
},
|
||||
{
|
||||
slug: "package-manifest",
|
||||
title: "Package And Manifest Reference",
|
||||
description: "zero.toml package manifests, imports, targets, dependencies, and profiles.",
|
||||
path: "/package-manifest",
|
||||
sourcePath: "/articles/package-manifest.md",
|
||||
section: "Language Pieces",
|
||||
},
|
||||
{
|
||||
slug: "standard-library",
|
||||
title: "Standard Library",
|
||||
description: "Graph-backed modules, capabilities, allocation behavior, and helper metadata.",
|
||||
path: "/standard-library",
|
||||
sourcePath: "/articles/standard-library.md",
|
||||
section: "Standard Library",
|
||||
},
|
||||
{
|
||||
slug: "target-capabilities",
|
||||
title: "Target Capabilities",
|
||||
description: "Current host and target-neutral capability boundaries.",
|
||||
path: "/target-capabilities",
|
||||
sourcePath: "/articles/target-capabilities.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "cross-compilation",
|
||||
title: "Cross-Compilation",
|
||||
description: "Targets, capability denial, direct artifacts, and target facts.",
|
||||
path: "/cross-compilation",
|
||||
sourcePath: "/articles/cross-compilation.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "optimization",
|
||||
title: "Optimization And Size Profiles",
|
||||
description: "Profile contracts, size breakdowns, retention reasons, optimization hints, and benchmark trends.",
|
||||
path: "/optimization",
|
||||
sourcePath: "/articles/optimization.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "benchmarks",
|
||||
title: "Benchmarks",
|
||||
description: "Benchmark methodology, cases, and metrics.",
|
||||
path: "/benchmarks",
|
||||
sourcePath: "/articles/benchmarks.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "c-interop",
|
||||
title: "C Interop",
|
||||
description: "Graph-backed C ABI export support and target library audit facts.",
|
||||
path: "/c-interop",
|
||||
sourcePath: "/articles/c-interop.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "building-from-source",
|
||||
title: "Building From Source",
|
||||
description: "Build, use, and validate the local compiler checkout.",
|
||||
path: "/native-compiler",
|
||||
sourcePath: "/articles/building-from-source.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "module-ascii",
|
||||
title: "std.ascii",
|
||||
description: "ASCII byte predicates, case conversion, and digit value helpers.",
|
||||
path: "/modules/ascii",
|
||||
sourcePath: "/articles/modules/ascii.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-parse",
|
||||
title: "std.parse",
|
||||
description: "Allocation-free byte scanners and integer/bool parsers.",
|
||||
path: "/modules/parse",
|
||||
sourcePath: "/articles/modules/parse.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-regex",
|
||||
title: "std.regex",
|
||||
description: "Compile-once regular expression matching for a documented ECMA-262-leaning subset.",
|
||||
path: "/modules/regex",
|
||||
sourcePath: "/articles/modules/regex.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-inet",
|
||||
title: "std.inet",
|
||||
description: "Target-neutral IPv4, IPv6, and RFC 1123 hostname literal validation and parsing.",
|
||||
path: "/modules/inet",
|
||||
sourcePath: "/articles/modules/inet.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-codec",
|
||||
title: "std.codec",
|
||||
description: "Little-endian integer helpers, unsigned varints, and CRC-32 primitives.",
|
||||
path: "/modules/codec",
|
||||
sourcePath: "/articles/modules/codec.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-csv",
|
||||
title: "std.csv",
|
||||
description: "Allocation-free CSV validation, record scanning, field decoding, and small writers.",
|
||||
path: "/modules/csv",
|
||||
sourcePath: "/articles/modules/csv.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-mem",
|
||||
title: "std.mem",
|
||||
description: "Span metadata, copy and equality helpers, and the allocator surface.",
|
||||
path: "/modules/mem",
|
||||
sourcePath: "/articles/modules/mem.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-collections",
|
||||
title: "std.collections",
|
||||
description: "Fixed-capacity collection operations over caller-owned storage.",
|
||||
path: "/modules/collections",
|
||||
sourcePath: "/articles/modules/collections.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-search",
|
||||
title: "std.search",
|
||||
description: "Scalar span search and binary-search helpers.",
|
||||
path: "/modules/search",
|
||||
sourcePath: "/articles/modules/search.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-sort",
|
||||
title: "std.sort",
|
||||
description: "In-place sort and sortedness helpers over caller-owned storage.",
|
||||
path: "/modules/sort",
|
||||
sourcePath: "/articles/modules/sort.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-args",
|
||||
title: "std.args",
|
||||
description: "Process argument count and indexed lookup for hosted command-line programs.",
|
||||
path: "/modules/args",
|
||||
sourcePath: "/articles/modules/args.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-cli",
|
||||
title: "std.cli",
|
||||
description: "Hosted flag and option helpers for command-line programs.",
|
||||
path: "/modules/cli",
|
||||
sourcePath: "/articles/modules/cli.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-diag",
|
||||
title: "std.diag",
|
||||
description: "Source offsets, line spans, and diagnostic location formatting.",
|
||||
path: "/modules/diag",
|
||||
sourcePath: "/articles/modules/diag.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-fmt",
|
||||
title: "std.fmt",
|
||||
description: "Caller-buffer formatting for booleans and integer text.",
|
||||
path: "/modules/fmt",
|
||||
sourcePath: "/articles/modules/fmt.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-math",
|
||||
title: "std.math",
|
||||
description: "Pure fixed-width integer helpers and small number-theory routines.",
|
||||
path: "/modules/math",
|
||||
sourcePath: "/articles/modules/math.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-path",
|
||||
title: "std.path",
|
||||
description: "Fixed-buffer path helpers with explicit storage and target-aware limits.",
|
||||
path: "/modules/path",
|
||||
sourcePath: "/articles/modules/path.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-str",
|
||||
title: "std.str",
|
||||
description: "Allocation-free byte-string helpers over spans and caller-owned storage.",
|
||||
path: "/modules/str",
|
||||
sourcePath: "/articles/modules/str.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-testing",
|
||||
title: "std.testing",
|
||||
description: "Bool-returning helpers for test blocks and output checks.",
|
||||
path: "/modules/testing",
|
||||
sourcePath: "/articles/modules/testing.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-term",
|
||||
title: "std.term",
|
||||
description: "ANSI terminal sequences, key-byte decoding, hosted terminal metadata, and raw mode.",
|
||||
path: "/modules/term",
|
||||
sourcePath: "/articles/modules/term.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-text",
|
||||
title: "std.text",
|
||||
description: "ASCII and UTF-8 byte-backed text validation.",
|
||||
path: "/modules/text",
|
||||
sourcePath: "/articles/modules/text.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
|
||||
{
|
||||
slug: "module-unicode",
|
||||
title: "std.unicode",
|
||||
description: "Strict UTF-8 codepoint decode/encode iteration and codepoint-class helpers.",
|
||||
path: "/modules/unicode",
|
||||
sourcePath: "/articles/modules/unicode.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-io",
|
||||
title: "std.io",
|
||||
description: "Buffered reader/writer helpers over caller-owned storage.",
|
||||
path: "/modules/io",
|
||||
sourcePath: "/articles/modules/io.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-fs",
|
||||
title: "std.fs",
|
||||
description: "Hosted file reads, writes, and existence checks for CLI programs.",
|
||||
path: "/modules/fs",
|
||||
sourcePath: "/articles/modules/fs.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-json",
|
||||
title: "std.json",
|
||||
description: "Validation, field lookup, explicit-allocator parsing, and caller-buffer writing.",
|
||||
path: "/modules/json",
|
||||
sourcePath: "/articles/modules/json.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-toml",
|
||||
title: "std.toml",
|
||||
description: "TOML validation, shallow field lookup, and typed scalar decode helpers.",
|
||||
path: "/modules/toml",
|
||||
sourcePath: "/articles/modules/toml.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-log",
|
||||
title: "std.log",
|
||||
description: "Explicit-buffer structured log record formatting.",
|
||||
path: "/modules/log",
|
||||
sourcePath: "/articles/modules/log.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-url",
|
||||
title: "std.url",
|
||||
description: "Lexical URL splitting, percent/query encoding, and query helpers.",
|
||||
path: "/modules/url",
|
||||
sourcePath: "/articles/modules/url.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-env",
|
||||
title: "std.env",
|
||||
description: "Hosted environment variable lookup.",
|
||||
path: "/modules/env",
|
||||
sourcePath: "/articles/modules/env.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-time",
|
||||
title: "std.time",
|
||||
description: "Duration math, hosted sleep, and target-gated monotonic and wall-clock helpers.",
|
||||
path: "/modules/time",
|
||||
sourcePath: "/articles/modules/time.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-rand",
|
||||
title: "std.rand",
|
||||
description: "Explicit deterministic random sources and target entropy helpers.",
|
||||
path: "/modules/rand",
|
||||
sourcePath: "/articles/modules/rand.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-proc",
|
||||
title: "std.proc",
|
||||
description: "Host process status helpers behind explicit process capability boundaries.",
|
||||
path: "/modules/proc",
|
||||
sourcePath: "/articles/modules/proc.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-pty",
|
||||
title: "std.pty",
|
||||
description: "Hosted pseudoterminal child processes for interactive CLIs and terminal programs.",
|
||||
path: "/modules/pty",
|
||||
sourcePath: "/articles/modules/pty.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-crypto",
|
||||
title: "std.crypto",
|
||||
description: "Hash, keyed hash, constant-time equality, and target entropy helpers.",
|
||||
path: "/modules/crypto",
|
||||
sourcePath: "/articles/modules/crypto.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-net",
|
||||
title: "std.net",
|
||||
description: "Network capability metadata, address builders, timeouts, and bootstrap handles.",
|
||||
path: "/modules/net",
|
||||
sourcePath: "/articles/modules/net.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-http",
|
||||
title: "std.http",
|
||||
description: "HTTP envelope writing, request parsing, hosted fetch, and response metadata helpers.",
|
||||
path: "/modules/http",
|
||||
sourcePath: "/articles/modules/http.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
];
|
||||
|
||||
export function findDocBySlug(slug: string): Doc | null {
|
||||
return docs.find((doc) => doc.slug === slug) ?? null;
|
||||
}
|
||||
|
||||
export function findDocByPath(path: string): Doc | null {
|
||||
return docs.find((doc) => doc.path === path) ?? null;
|
||||
}
|
||||
|
||||
export function groupBySection(items: Doc[]): DocsGroup[] {
|
||||
const groups: DocsGroup[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of items) {
|
||||
const section = item.section ?? "Other";
|
||||
if (!seen.has(section)) {
|
||||
seen.add(section);
|
||||
groups.push({ section, items: [] });
|
||||
}
|
||||
const group = groups.find((g) => g.section === section);
|
||||
if (group) group.items.push(item);
|
||||
}
|
||||
return groups.sort((a, b) => {
|
||||
const ai = SECTION_ORDER.indexOf(a.section);
|
||||
const bi = SECTION_ORDER.indexOf(b.section);
|
||||
if (ai === -1 && bi === -1) return 0;
|
||||
if (ai === -1) return 1;
|
||||
if (bi === -1) return -1;
|
||||
return ai - bi;
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdjacentDocs(slug: string): { prev: Doc | null; next: Doc | null } {
|
||||
const index = docs.findIndex((d) => d.slug === slug);
|
||||
if (index === -1) return { prev: null, next: null };
|
||||
return {
|
||||
prev: index > 0 ? docs[index - 1] : null,
|
||||
next: index < docs.length - 1 ? docs[index + 1] : null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
const REPO = process.env.NEXT_PUBLIC_GITHUB_REPO || "vercel-labs/zerolang";
|
||||
const REVALIDATE = 86400;
|
||||
|
||||
export async function getStarCount(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
|
||||
headers: { Accept: "application/vnd.github.v3+json" },
|
||||
next: { revalidate: REVALIDATE },
|
||||
});
|
||||
if (!res.ok) return "";
|
||||
const data = (await res.json()) as { stargazers_count?: unknown };
|
||||
const count = data.stargazers_count;
|
||||
if (typeof count !== "number") return "";
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
|
||||
return String(count);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
type TokenName =
|
||||
| "comment"
|
||||
| "string"
|
||||
| "char"
|
||||
| "keyword"
|
||||
| "type"
|
||||
| "function"
|
||||
| "number"
|
||||
| "variable"
|
||||
| "id"
|
||||
| "key"
|
||||
| "boolean"
|
||||
| "punctuation"
|
||||
| "operator";
|
||||
|
||||
type Grammar = ReadonlyArray<readonly [TokenName, RegExp]>;
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
const GRAMMARS: Record<string, Grammar> = {
|
||||
bash: [
|
||||
["string", /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/],
|
||||
["comment", /#.*/],
|
||||
["variable", /\$\{?[A-Za-z_][A-Za-z0-9_]*\}?/],
|
||||
["key", /--?[A-Za-z0-9][A-Za-z0-9-]*(?:=[^\s\\]+)?/],
|
||||
["keyword", /\b(?:if|then|else|elif|fi|for|while|do|done|case|esac|function|export|local|readonly|return|exit|set|unset)\b/],
|
||||
["function", /(?<![A-Za-z0-9_./-])(?:zero|pnpm|npm|git|curl|make|node|npx|bash|sh|zsh|cd|mkdir|rm|cp|mv|cat|sed|rg)(?=\s|$)/],
|
||||
["number", /\b\d+\b/],
|
||||
["operator", /&&|\|\||\\|[|<>;&=]/],
|
||||
["punctuation", /[{}()[\]]/],
|
||||
],
|
||||
zero: [
|
||||
["comment", /\/\/.*/],
|
||||
["string", /"(?:[^"\\]|\\.)*"/],
|
||||
["char", /'(?:[^'\\\n]|\\(?:[nrt0'"\\]|x[0-9A-Fa-f]{2}))'/],
|
||||
["keyword", /\b(?:pub|fn|let|var|return|raises|if|else|while|for|in|match|check|rescue|raise|use|interface|enum|choice|type|alias|const|as|break|continue|defer|export|extern|packed|static|meta|mut|test|and|or|not|true|false|null)\b/],
|
||||
["type", /\b(?:Void|World|Self|Bool|bool|char|u4|u8|u16|u32|u64|i8|i16|i32|i64|usize|isize|f16|f32|f64|Span|MutSpan|Maybe|Alloc|Arena|FixedBufAlloc|GeneralAlloc|NullAlloc|PageAlloc|ref|mutref|owned|Slice|Array|String|Error|Io|Fs|Net|Env|Args|Clock|Rand|Proc|Sync|Cancel|Reader|Writer|File|Path|Conn|Listener|Address|Config|Type|Field|c_int|c_long|c_size|cstr|[A-Z][A-Za-z0-9_]*)\b/],
|
||||
["function", /\b[a-z_]\w*(?=\s*\()/],
|
||||
["number", /\b(?:\d+\.\d+(?:[eE][+-]?\d+)?|0x[0-9a-fA-F_]+|0b[01_]+|0o[0-7_]+|\d[\d_]*(?:_[A-Za-z][A-Za-z0-9_]*)?)\b/],
|
||||
["variable", /\b[a-z_]\w*(?=\.)/],
|
||||
["punctuation", /[{}()\[\];,.<>]/],
|
||||
["operator", /:|[-+*/%=!&|^~@?]+/],
|
||||
],
|
||||
"zero-graph": [
|
||||
["comment", /\/\/.*/],
|
||||
["string", /"(?:[^"\\]|\\.)*"/],
|
||||
["id", /#[A-Za-z_][A-Za-z0-9_]*/],
|
||||
["keyword", /\b(?:zero-graph|zero-program-graph-patch|origin|module|hash|node|edge|expect|graphHash|set|insert|insertEdge|replace|delete|rename|addFunction|addMain|addParam|addReturnBinary|addLetLiteral|addLetBinary|addReturnValue|addCheckWrite|addCheckWriteValue|addTest|replaceFunctionBody|replaceBlockBody|end|let|var|return|if|else|while|for|in|match|check|rescue|raise|use|test|and|or|not)\b/],
|
||||
["type", /\b(?:Function|Param|Block|Identifier|Literal|MethodCall|Call|FieldAccess|Let|Var|Return|Check|If|While|For|Match|TypeRef|ArrayLiteral|Unary|Binary|Void|World|Self|Bool|bool|char|u4|u8|u16|u32|u64|i8|i16|i32|i64|usize|isize|f16|f32|f64|Span|MutSpan|Maybe|Alloc|Arena|FixedBufAlloc|GeneralAlloc|NullAlloc|PageAlloc|ref|mutref|owned|Slice|Array|String|Error|[A-Z][A-Za-z0-9_]*)\b/],
|
||||
["boolean", /\b(?:true|false|null)\b/],
|
||||
["key", /\b[A-Za-z_][A-Za-z0-9_-]*(?=\s*(?::|=))/],
|
||||
["function", /\b(?:std|[a-z_][A-Za-z0-9_]*)\.[A-Za-z_][A-Za-z0-9_.]*(?=\s|$)/],
|
||||
["number", /\b(?:v\d+|graph:[0-9a-fA-F]+|nodehash:[0-9a-fA-F]+|0x[0-9a-fA-F_]+|0b[01_]+|0o[0-7_]+|\d[\d_]*)\b/],
|
||||
["variable", /\b[a-z_][A-Za-z0-9_]*(?=\.)/],
|
||||
["punctuation", /[{}()\[\];,.<>]/],
|
||||
["operator", /:|=|[-+*/%!&|^~@?]+/],
|
||||
],
|
||||
};
|
||||
|
||||
const LANGUAGE_ALIASES: Record<string, string> = {
|
||||
sh: "bash",
|
||||
shell: "bash",
|
||||
zsh: "bash",
|
||||
graph: "zero-graph",
|
||||
"program-graph": "zero-graph",
|
||||
"zero-program-graph": "zero-graph",
|
||||
"graph-patch": "zero-graph",
|
||||
"zero-graph-patch": "zero-graph",
|
||||
"program-graph-patch": "zero-graph",
|
||||
"zero-program-graph-patch": "zero-graph",
|
||||
};
|
||||
|
||||
function normalizeLanguage(language: string): string {
|
||||
const key = language.trim().toLowerCase();
|
||||
return LANGUAGE_ALIASES[key] ?? key;
|
||||
}
|
||||
|
||||
function looksLikeGraph(code: string): boolean {
|
||||
const text = code.trimStart();
|
||||
return (
|
||||
/^zero-graph\s+v\d+\b/.test(text) ||
|
||||
/^zero-program-graph-patch\s+v\d+\b/.test(text) ||
|
||||
/^(?:node|edge)\s+#[A-Za-z_][A-Za-z0-9_]*\b/m.test(text) ||
|
||||
/^(?:expect\s+graphHash|set\s+node=|insert\s+node=|insertEdge\s+|replace\s+node=|rename\s+node=|delete\s+node=|replaceFunctionBody\s+|replaceBlockBody\s+)/m.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
export function highlightLanguage(language: string, code: string): string | null {
|
||||
const normalized = normalizeLanguage(language);
|
||||
if (GRAMMARS[normalized]) return normalized;
|
||||
if ((normalized === "" || normalized === "text" || normalized === "txt") && looksLikeGraph(code)) {
|
||||
return "zero-graph";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function highlight(code: string, language: string): string {
|
||||
const normalized = highlightLanguage(language, code);
|
||||
const grammar = normalized ? GRAMMARS[normalized] : null;
|
||||
if (!grammar) return escapeHtml(code);
|
||||
|
||||
const combined = new RegExp(
|
||||
grammar.map(([, pattern]) => `(${pattern.source})`).join("|"),
|
||||
"gm",
|
||||
);
|
||||
|
||||
const names = grammar.map(([name]) => name);
|
||||
let result = "";
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of code.matchAll(combined)) {
|
||||
const matchIndex = match.index ?? 0;
|
||||
if (matchIndex > lastIndex) {
|
||||
result += escapeHtml(code.slice(lastIndex, matchIndex));
|
||||
}
|
||||
const groupIndex = match.slice(1).findIndex((g) => g !== undefined);
|
||||
if (groupIndex === -1) continue;
|
||||
const tokenType = names[groupIndex];
|
||||
const tokenText = match[0];
|
||||
result += `<span class="hl-${tokenType} hl-${normalized}-${tokenType}">${escapeHtml(tokenText)}</span>`;
|
||||
lastIndex = matchIndex + tokenText.length;
|
||||
}
|
||||
|
||||
if (lastIndex < code.length) {
|
||||
result += escapeHtml(code.slice(lastIndex));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PAGE_TITLES } from "./page-titles";
|
||||
import { findDocByPath } from "./docs";
|
||||
|
||||
const SITE_NAME = "Zero";
|
||||
const DESCRIPTION =
|
||||
"zerolang is an experimental graph-first programming language where agents work with semantic program structure instead of raw source text.";
|
||||
|
||||
export function pageMetadata(slug: string): Metadata {
|
||||
const title = PAGE_TITLES[slug];
|
||||
if (title === undefined) return {};
|
||||
|
||||
const displayTitle = title.replace(/\n/g, " ");
|
||||
const fullTitle = slug === "" ? `${SITE_NAME} | ${displayTitle}` : `${displayTitle} | ${SITE_NAME}`;
|
||||
const ogImageUrl = slug ? `/og/${slug}` : "/og";
|
||||
|
||||
const doc = slug ? findDocByPath(`/${slug}`) : null;
|
||||
const description = doc?.description ?? DESCRIPTION;
|
||||
|
||||
return {
|
||||
title: slug === "" ? fullTitle : displayTitle,
|
||||
description,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: SITE_NAME,
|
||||
title: fullTitle,
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${displayTitle} — ${SITE_NAME}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: fullTitle,
|
||||
description,
|
||||
images: [ogImageUrl],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { docs } from "./docs";
|
||||
|
||||
const DOCS_TITLES: Record<string, string> = Object.fromEntries(
|
||||
docs.map((doc) => [doc.path.replace(/^\//, ""), doc.title]),
|
||||
);
|
||||
|
||||
export const PAGE_TITLES: Record<string, string> = {
|
||||
"": "An agent-first language experiment.",
|
||||
...DOCS_TITLES,
|
||||
};
|
||||
|
||||
export function getPageTitle(slug: string): string | null {
|
||||
return slug in PAGE_TITLES ? PAGE_TITLES[slug] : null;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { Redis } from "@upstash/redis";
|
||||
|
||||
let minuteRateLimit: Ratelimit | null = null;
|
||||
let dailyRateLimit: Ratelimit | null = null;
|
||||
|
||||
export function isDocsChatRateLimitConfigured() {
|
||||
return Boolean(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
|
||||
}
|
||||
|
||||
function getRedis() {
|
||||
const url = process.env.KV_REST_API_URL;
|
||||
const token = process.env.KV_REST_API_TOKEN;
|
||||
|
||||
if (!url || !token) {
|
||||
throw new Error("Docs chat rate limiting requires KV_REST_API_URL and KV_REST_API_TOKEN");
|
||||
}
|
||||
|
||||
return new Redis({ url, token });
|
||||
}
|
||||
|
||||
function readPositiveInt(name: string, fallback: number): number {
|
||||
const value = Number(process.env[name]);
|
||||
return Number.isInteger(value) && value > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
const MINUTE_LIMIT = readPositiveInt("RATE_LIMIT_PER_MINUTE", 10);
|
||||
const DAILY_LIMIT = readPositiveInt("RATE_LIMIT_PER_DAY", 100);
|
||||
|
||||
export const docsChatMinuteRateLimit = {
|
||||
limit: async (identifier: string) => {
|
||||
if (!minuteRateLimit) {
|
||||
const redis = getRedis();
|
||||
minuteRateLimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
|
||||
prefix: "ratelimit:docs-chat:minute",
|
||||
});
|
||||
}
|
||||
return minuteRateLimit.limit(identifier);
|
||||
},
|
||||
};
|
||||
|
||||
export const docsChatDailyRateLimit = {
|
||||
limit: async (identifier: string) => {
|
||||
if (!dailyRateLimit) {
|
||||
const redis = getRedis();
|
||||
dailyRateLimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
|
||||
prefix: "ratelimit:docs-chat:daily",
|
||||
});
|
||||
}
|
||||
return dailyRateLimit.limit(identifier);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
// Rewrites ```json-render fenced code blocks into custom elements carrying
|
||||
// base64-encoded JSON, so the docs can embed structured React components
|
||||
// without switching the pipeline to MDX. Runs before rehype-pretty-code so the
|
||||
// block is never treated as a normal code fence.
|
||||
|
||||
type HastNode = {
|
||||
type?: string;
|
||||
tagName?: string;
|
||||
value?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
children?: HastNode[];
|
||||
};
|
||||
|
||||
function getNodeText(node?: HastNode): string {
|
||||
if (!node) return "";
|
||||
if (node.type === "text") return node.value ?? "";
|
||||
if (!Array.isArray(node.children)) return "";
|
||||
return node.children.map(getNodeText).join("");
|
||||
}
|
||||
|
||||
function getCodeLanguage(node: HastNode): string {
|
||||
const className = node.properties?.className;
|
||||
const classes = Array.isArray(className)
|
||||
? className
|
||||
: typeof className === "string"
|
||||
? className.split(/\s+/)
|
||||
: [];
|
||||
const languageClass = classes.find(
|
||||
(value) => typeof value === "string" && value.startsWith("language-"),
|
||||
);
|
||||
return typeof languageClass === "string" ? languageClass.slice("language-".length) : "";
|
||||
}
|
||||
|
||||
function visit(node: HastNode | undefined, callback: (node: HastNode) => void): void {
|
||||
if (!node) return;
|
||||
callback(node);
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) visit(child, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function rehypeJsonRender(): (tree: HastNode) => void {
|
||||
return (tree: HastNode) => {
|
||||
visit(tree, (node) => {
|
||||
if (node.type !== "element" || node.tagName !== "pre") return;
|
||||
const code = node.children?.find(
|
||||
(child) => child.type === "element" && child.tagName === "code",
|
||||
);
|
||||
if (!code || getCodeLanguage(code) !== "json-render") return;
|
||||
|
||||
const raw = getNodeText(code).replace(/\n$/, "");
|
||||
let tagName = "agentchat";
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { type?: string };
|
||||
if (parsed.type === "flow") tagName = "flowchart";
|
||||
} catch {}
|
||||
node.tagName = tagName;
|
||||
node.properties = { value: Buffer.from(raw, "utf8").toString("base64") };
|
||||
node.children = [];
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { highlight, highlightLanguage } from "./highlight";
|
||||
|
||||
type HastNode = {
|
||||
type?: string;
|
||||
tagName?: string;
|
||||
value?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
children?: HastNode[];
|
||||
};
|
||||
|
||||
type NodePredicate = (node: HastNode) => boolean;
|
||||
type NodeCallback = (node: HastNode) => void;
|
||||
|
||||
const ZERO_TOKEN_COLORS: Record<string, { light: string; dark: string }> = {
|
||||
comment: { light: "#6A737D", dark: "#6A737D" },
|
||||
string: { light: "#032F62", dark: "#9ECBFF" },
|
||||
char: { light: "#032F62", dark: "#9ECBFF" },
|
||||
keyword: { light: "#D73A49", dark: "#F97583" },
|
||||
type: { light: "#005CC5", dark: "#79B8FF" },
|
||||
function: { light: "#6F42C1", dark: "#B392F0" },
|
||||
number: { light: "#005CC5", dark: "#79B8FF" },
|
||||
variable: { light: "#24292E", dark: "#E1E4E8" },
|
||||
id: { light: "#6F42C1", dark: "#B392F0" },
|
||||
key: { light: "#D73A49", dark: "#F97583" },
|
||||
boolean: { light: "#005CC5", dark: "#79B8FF" },
|
||||
punctuation: { light: "#24292E", dark: "#E1E4E8" },
|
||||
operator: { light: "#D73A49", dark: "#F97583" },
|
||||
};
|
||||
|
||||
function visit(node: HastNode | undefined, predicate: NodePredicate, callback: NodeCallback): void {
|
||||
if (!node) return;
|
||||
if (predicate(node)) callback(node);
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) visit(child, predicate, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function highlightToHast(code: string, language: string): HastNode[] {
|
||||
const parts = highlight(code, language).split("\n");
|
||||
const lines = [];
|
||||
for (let li = 0; li < parts.length; li++) {
|
||||
const lineHtml = parts[li];
|
||||
const tokens = [];
|
||||
const tokenRegex = /<span class="hl-([A-Za-z0-9_-]+)(?:\s+[^"]*)?">([\s\S]*?)<\/span>|([^<]+)/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = tokenRegex.exec(lineHtml)) !== null) {
|
||||
if (match[1]) {
|
||||
const color = ZERO_TOKEN_COLORS[match[1]];
|
||||
const text = decodeEntities(match[2]);
|
||||
tokens.push({
|
||||
type: "element",
|
||||
tagName: "span",
|
||||
properties: color
|
||||
? { style: `--shiki-light:${color.light};--shiki-dark:${color.dark}` }
|
||||
: {},
|
||||
children: [{ type: "text", value: text }],
|
||||
});
|
||||
} else if (match[3]) {
|
||||
tokens.push({ type: "text", value: decodeEntities(match[3]) });
|
||||
}
|
||||
}
|
||||
lines.push({
|
||||
type: "element",
|
||||
tagName: "span",
|
||||
properties: { "data-line": "" },
|
||||
children: tokens.length > 0 ? tokens : [{ type: "text", value: "" }],
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function decodeEntities(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function getNodeText(node: HastNode | undefined): string {
|
||||
if (!node) return "";
|
||||
if (node.type === "text") return node.value ?? "";
|
||||
if (!Array.isArray(node.children)) return "";
|
||||
return node.children.map(getNodeText).join("");
|
||||
}
|
||||
|
||||
function getCodeLanguage(node: HastNode): string {
|
||||
const dataLanguage = node.properties?.dataLanguage ?? node.properties?.["data-language"];
|
||||
if (typeof dataLanguage === "string") return dataLanguage;
|
||||
const className = node.properties?.className;
|
||||
const classes = Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : [];
|
||||
const languageClass = classes.find((value) => typeof value === "string" && value.startsWith("language-"));
|
||||
return typeof languageClass === "string" ? languageClass.slice("language-".length) : "";
|
||||
}
|
||||
|
||||
export function rehypeZeroHighlight(): (tree: HastNode) => void {
|
||||
return (tree: HastNode) => {
|
||||
visit(
|
||||
tree,
|
||||
(node) =>
|
||||
node.type === "element" &&
|
||||
node.tagName === "code",
|
||||
(codeNode) => {
|
||||
const text = getNodeText(codeNode).replace(/\n$/, "");
|
||||
const language = highlightLanguage(getCodeLanguage(codeNode), text);
|
||||
if (!language || language === "bash") return;
|
||||
codeNode.children = highlightToHast(text, language);
|
||||
codeNode.properties = {
|
||||
...codeNode.properties,
|
||||
style: "display:grid",
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { readArticleBySlug } from "./articles";
|
||||
import { docs } from "./docs";
|
||||
import type { SearchIndexEntry } from "./types";
|
||||
|
||||
let cached: SearchIndexEntry[] | null = null;
|
||||
|
||||
function stripMarkdown(md: string): string {
|
||||
return md
|
||||
.replace(/```[\s\S]*?```/g, "")
|
||||
.replace(/`[^`]+`/g, "")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export async function getSearchIndex(): Promise<SearchIndexEntry[]> {
|
||||
if (cached) return cached;
|
||||
|
||||
const entries: SearchIndexEntry[] = [];
|
||||
for (const doc of docs) {
|
||||
try {
|
||||
const raw = await readArticleBySlug(doc.slug);
|
||||
const content = raw ? stripMarkdown(raw) : "";
|
||||
entries.push({
|
||||
title: doc.title,
|
||||
href: doc.path,
|
||||
section: doc.section ?? "",
|
||||
content,
|
||||
});
|
||||
} catch {
|
||||
entries.push({
|
||||
title: doc.title,
|
||||
href: doc.path,
|
||||
section: doc.section ?? "",
|
||||
content: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cached = entries;
|
||||
return entries;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export type Doc = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
path: `/${string}`;
|
||||
sourcePath: `/articles/${string}.md`;
|
||||
section: string;
|
||||
};
|
||||
|
||||
export type DocsGroup = {
|
||||
section: string;
|
||||
items: Doc[];
|
||||
};
|
||||
|
||||
export type Heading = {
|
||||
level: 2 | 3 | 4;
|
||||
text: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Article = {
|
||||
doc: Doc;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type SearchIndexEntry = {
|
||||
title: string;
|
||||
href: string;
|
||||
section: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
title: string;
|
||||
href: string;
|
||||
section: string;
|
||||
snippet: string;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { clsx } from "clsx";
|
||||
import type { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user