Compare commits

..

6 Commits

Author SHA1 Message Date
codex-bot 787491622b Replace collection select with horizontal category dropdown navigation
Verify and deploy Tabularium / verify-and-deploy (pull_request) Successful in 11m20s
2026-10-28 19:38:07 +08:00
Zayden-Jung e448acb904 Merge pull request '按分类与合集组织文档 URL,收紧导航与阅读排版' (#2) from codex/docs-navigation into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m33s
Reviewed-on: #2
2026-10-28 11:16:16 +00:00
codex-bot e286139572 Redirect legacy document URLs after authentication without prerendering aliases
Verify and deploy Tabularium / verify-and-deploy (pull_request) Successful in 5m25s
2026-10-28 19:15:31 +08:00
codex-bot c49c8348d2 Merge published comic changes and adapt navigation verification
Verify and deploy Tabularium / verify-and-deploy (pull_request) Failing after 5m28s
2026-10-28 19:02:26 +08:00
Zayden-Jung 7a9c88a05a Merge pull request '为 Endor 项目讨论增加双图漫画与共享图片压缩策略' (#3) from codex/endor-review-comic into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 4m34s
Reviewed-on: #3
2026-10-28 10:57:58 +00:00
codex-bot 422d6622a2 Organize document URLs by category and collection with compact navigation
Verify and deploy Tabularium / verify-and-deploy (pull_request) Has been cancelled
2026-10-28 18:38:13 +08:00
10 changed files with 340 additions and 48 deletions
+25 -4
View File
@@ -184,17 +184,38 @@ async function main() {
const searchResponse = await request("/api/search", { headers });
assert.equal(searchResponse.status, 200);
const searchIndex = await searchResponse.json();
assert.ok(searchIndex.some((doc: { slug: string; searchText: string }) => doc.slug === "pretext/04-chapter" && doc.searchText.includes("getSegmentMetrics")));
assert.ok(searchIndex.some((doc: { slug: string; searchText: string }) => doc.slug === "source-studies/pretext/measurement-cache" && doc.searchText.includes("getSegmentMetrics")));
for (const doc of searchIndex as { slug: string; sourceSlug: string }[]) {
assert.ok(!doc.slug.split("/").some((part) => /^\d+-/.test(part)), doc.slug);
const legacy = await request(`/docs/${doc.sourceSlug}?from=legacy`, { headers });
assert.equal(legacy.status, 308, doc.sourceSlug);
const destination = new URL(legacy.headers.get("location")!, base);
assert.equal(destination.pathname, `/docs/${doc.slug}`);
assert.equal(destination.searchParams.get("from"), "legacy");
assert.ok(legacy.headers.get("cache-control")?.includes("no-store"));
}
let home = "";
let purpose = "";
for (const route of routes) {
const response = await request(route, { headers });
assert.equal(response.status, 200, route);
if (route === "/") home = await response.text();
if (route === "/docs/04-endor-purpose-draft") purpose = await response.text();
const html = await response.text();
if (route === "/") home = html;
if (route === "/docs/endor/endor-purpose-draft") purpose = html;
if (route.startsWith("/docs/")) {
assert.ok(html.includes('aria-label="文档分类"'), route);
assert.ok(html.includes('data-category="语言与运行时"'), route);
assert.ok(html.includes('class="category-panel" hidden=""'), route);
assert.ok(!html.includes('class="collection-picker"'), route);
assert.ok(!html.includes("<select"), route);
assert.ok(!html.includes('class="nav-back"'), route);
const ids = [...html.matchAll(/<h[23] id="([^"]+)"/g)].map((match) => match[1]);
assert.equal(new Set(ids).size, ids.length, `Unique heading anchors: ${route}`);
for (const id of ids) assert.ok(html.includes(`href="#${id}"`), `TOC anchor: ${route}#${id}`);
}
}
assert.ok(home.includes("资料库"));
assert.ok(home.includes("/docs/pretext/00-overview"));
assert.ok(home.includes("/docs/source-studies/pretext/overview"));
// Inline preview and zoom dialog must both use the compressed browser asset.
assert.equal(
[...purpose.matchAll(/<img\b[^>]*\bsrc="\/figures\/endor-review-gate\.webp"/g)].length,
@@ -0,0 +1,93 @@
import Link from "next/link";
import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react";
import type { DocLink } from "@/lib/docs";
import { collectionEntries, collectionIcon, collectionLabel, collectionTone } from "@/lib/collections";
import { Icon } from "./Icon";
export function CategoryNavigation({ docs, current }: { docs: DocLink[]; current?: string }) {
const navigation = useRef<HTMLElement>(null);
const triggers = useRef(new Map<string, HTMLButtonElement>());
const [open, setOpen] = useState<string | null>(null);
const id = useId();
const categories = [...new Set(docs.map((doc) => doc.category))];
const collections = collectionEntries(docs);
const selected = docs.find((doc) => doc.slug === current);
useEffect(() => {
function outside(event: PointerEvent) {
if (event.target instanceof Node && !navigation.current?.contains(event.target)) setOpen(null);
}
function escape(event: globalThis.KeyboardEvent) {
if (event.key !== "Escape" || !open) return;
if (navigation.current?.contains(document.activeElement)) {
event.preventDefault();
triggers.current.get(open)?.focus();
}
setOpen(null);
}
document.addEventListener("pointerdown", outside);
document.addEventListener("keydown", escape);
return () => {
document.removeEventListener("pointerdown", outside);
document.removeEventListener("keydown", escape);
};
}, [open]);
function focusItem(panel: string, last = false) {
requestAnimationFrame(() => {
const links = document.getElementById(panel)?.querySelectorAll<HTMLAnchorElement>("a");
links?.[last ? links.length - 1 : 0]?.focus();
});
}
function move(event: KeyboardEvent<HTMLDivElement>) {
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
const links = [...event.currentTarget.querySelectorAll<HTMLAnchorElement>("a")];
const index = links.indexOf(document.activeElement as HTMLAnchorElement);
if (index < 0) return;
event.preventDefault();
const next = event.key === "Home" ? 0 : event.key === "End" ? links.length - 1 :
(index + (event.key === "ArrowDown" ? 1 : -1) + links.length) % links.length;
links[next]?.focus();
}
return <nav ref={navigation} className="top-nav category-navigation" aria-label="文档分类"
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(null);
}}>
<ul>
<li><Link className="category-home" href="/" aria-current={!current ? "page" : undefined} onClick={() => setOpen(null)}>文档中心</Link></li>
{categories.map((category, index) => {
const panel = `${id}-category-${index}`;
const expanded = open === category;
return <li className="category-menu" key={category}
onPointerEnter={(event) => { if (event.pointerType === "mouse") setOpen(category); }}
onPointerLeave={(event) => { if (event.pointerType === "mouse") setOpen(null); }}>
<button ref={(element) => { if (element) triggers.current.set(category, element); else triggers.current.delete(category); }}
type="button" className="category-trigger" data-category={category}
data-active={selected?.category === category || undefined}
aria-expanded={expanded} aria-controls={panel}
onClick={() => setOpen(expanded ? null : category)}
onKeyDown={(event) => {
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
setOpen(category);
focusItem(panel, event.key === "ArrowUp");
}
}}>
{category}<Icon name="chevron" size={11} />
</button>
<div id={panel} className="category-panel" hidden={!expanded} onKeyDown={move}>
<p className="category-panel-title">{category}</p>
{collections.filter(({entry}) => entry.category === category).map(({name, entry}) => <Link
key={name} href={`/docs/${entry.slug}`} data-tone={collectionTone(name)}
aria-current={current === entry.slug ? "page" : selected?.collection === name ? "true" : undefined}
onClick={() => setOpen(null)}>
<Icon name={collectionIcon(name)} size={19} />
<span><strong>{collectionLabel(name)}</strong><small>{entry.description}</small></span>
</Link>)}
</div>
</li>;
})}
</ul>
</nav>;
}
+3 -9
View File
@@ -1,4 +1,5 @@
import Link from "next/link";
import { CategoryNavigation } from "./CategoryNavigation";
import { Icon } from "@/components/Icon";
import { useEffect, useRef, useState, type ReactNode } from "react";
import type { DocLink } from "@/lib/docs";
@@ -36,9 +37,6 @@ export function Navigation({
return (
<nav aria-label="文档导航">
{collection ? <>
<Link className="nav-back" href="/" onClick={onNavigate}>
<Icon name="arrow" size={15} style={{ transform: "rotate(180deg)" }} />全部文档
</Link>
<Link className="collection-identity" href={`/docs/${collections.find((c) => c.name === collection)!.entry.slug}`} onClick={onNavigate}>
<Icon name={collectionIcon(collection)} size={22} /><span>{collectionLabel(collection)}</span>
</Link>
@@ -52,7 +50,7 @@ export function Navigation({
{scopedDocs.filter((d) => d.group === group).map((d) => (
<Link onClick={onNavigate} aria-current={d.slug === current ? "page" : undefined}
className="nav-item" href={`/docs/${d.slug}`} key={d.slug}>
<span>{d.slug.endsWith("/00-overview") ? "概览" : d.title.replace("(草案)", "")}</span>
<span>{d.slug.endsWith("/overview") ? "概览" : d.title.replace("(草案)", "").replace(/^\d+\s*·\s*/, "")}</span>
{d.draft && <span className="tiny-dot" aria-label="草案" />}
</Link>
))}
@@ -81,7 +79,6 @@ export function Shell({
const [selected, setSelected] = useState(0);
const [menu, setMenu] = useState(false);
const collection = docs.find((d) => d.slug === current)?.collection;
const collections = collectionEntries(docs);
const results = searchDocs.filter((d) =>
(!collection || d.collection === collection) &&
`${d.title} ${d.description} ${d.searchText ?? ""}`
@@ -126,10 +123,7 @@ export function Shell({
</a>
<header className="site-header">
<Brand />
<nav className="top-nav" aria-label="主导航">
<Link href="/" aria-current={!current ? "page" : undefined}>文档中心</Link>
{collections.map(({ name, entry }) => <Link key={name} href={`/docs/${entry.slug}`} aria-current={collection === name ? "page" : undefined}>{collectionLabel(name)}</Link>)}
</nav>
<CategoryNavigation docs={docs} current={current} />
<button
ref={trigger}
className="search-trigger"
+1 -1
View File
@@ -18,7 +18,7 @@ export function collectionIcon(name?: string): "book" | "code" | "layers" | "nod
export function collectionEntries(docs: DocLink[]) {
return [...new Set(docs.map((doc) => doc.collection))].map((name) => {
const items = docs.filter((doc) => doc.collection === name);
const entry = items.find((doc) => doc.slug.endsWith("/00-overview")) ?? items[0];
const entry = items.find((doc) => doc.slug.endsWith("/overview")) ?? items[0];
return { name, entry, count: items.length };
});
}
+13 -7
View File
@@ -4,20 +4,24 @@ import catalog from "../../../../content/catalog.json";
const directory = path.resolve(process.cwd(), "../../content/docs");
const validSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
type Metadata = { collection?: string; group?: string; description?: string; draft?: boolean; featured?: boolean };
type Metadata = { slug?: string; category?: string; collection?: string; group?: string; description?: string; draft?: boolean; featured?: boolean };
export type DocLink = {
slug: string; title: string; searchText?: string;
slug: string; sourceSlug: string; category: string; title: string; searchText?: string;
group: string; collection: string; description: string; draft: boolean; featured: boolean;
};
export type Doc = DocLink & { content: string };
export async function readDoc(slug: string): Promise<Doc> {
if (!validSlug.test(slug)) throw new Error("Invalid document slug");
const content = await fs.readFile(path.join(directory, `${slug}.md`), "utf8");
const sourceSlug = Object.entries(catalog.documents).find(([, metadata]) => metadata.slug === slug)?.[0] ?? slug;
const content = await fs.readFile(path.join(directory, `${sourceSlug}.md`), "utf8");
const title = content.match(/^# (.+)$/m)?.[1]?.trim() ?? slug;
const collection = catalog.collections.find((c) => !c.path || slug.startsWith(`${c.path}/`))!;
const metadata: Metadata = (catalog.documents as Record<string, Metadata>)[slug] ?? {};
return { slug, title, content, searchText: content,
const collection = catalog.collections.find((c) => !c.path || sourceSlug.startsWith(`${c.path}/`))!;
const metadata: Metadata = (catalog.documents as Record<string, Metadata>)[sourceSlug] ?? {};
if (!metadata.slug || !metadata.category || !validSlug.test(metadata.slug) || metadata.slug.split("/").some((part) => /^\d+-/.test(part))) {
throw new Error(`Register a semantic slug and category in content/catalog.json: ${sourceSlug}`);
}
return { slug: metadata.slug, sourceSlug, category: metadata.category, title, content, searchText: content,
group: metadata.group ?? collection.title, collection: metadata.collection ?? collection.title,
description: metadata.description ?? collection.description,
draft: metadata.draft ?? false, featured: metadata.featured ?? false,
@@ -36,9 +40,11 @@ export async function listDocs(): Promise<DocLink[]> {
}));
return files.flat();
}
return Promise.all((await walk("")).sort().map(async (slug) => {
const docs = await Promise.all((await walk("")).sort().map(async (slug) => {
const { content: _, searchText: __, ...doc } = await readDoc(slug);
void _; void __;
return doc;
}));
if (new Set(docs.map((doc) => doc.slug)).size !== docs.length) throw new Error("Duplicate public document slug");
return docs;
}
+13 -5
View File
@@ -103,6 +103,17 @@ export default function DocPage({
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children }) => {
if (!href) return <a>{children}</a>;
let target = href;
if (!/^(?:[a-z]+:|\/\/|#)/i.test(href)) {
const resolved = new URL(href, `https://docs.local/docs/${doc.sourceSlug}`);
const source = decodeURIComponent(resolved.pathname).replace(/^\/docs\//, "").replace(/\.md$/, "");
const linked = docs.find((d) => d.sourceSlug === source || d.slug === source);
if (linked) target = `/docs/${linked.slug}${resolved.search}${resolved.hash}`;
}
return <a href={target}>{children}</a>;
},
p: ({ children, node }) => {
const slides = node?.children.flatMap((child) =>
child.type === "element" && child.tagName === "img" &&
@@ -217,7 +228,7 @@ export default function DocPage({
<div className="article-end">
<a
href={`https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs/src/branch/main/content/docs/${doc.slug}.md`}
href={`https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs/src/branch/main/content/docs/${doc.sourceSlug}.md`}
>
查看文档源码 ↗
</a>
@@ -229,10 +240,7 @@ export default function DocPage({
<strong>{siblings[index - 1].title}</strong>
</Link>
) : (
<Link href="/">
<small>← 返回</small>
<strong>文档概览</strong>
</Link>
<span />
)}
{index < siblings.length - 1 && (
<Link
+12
View File
@@ -1,5 +1,10 @@
import { NextResponse, type NextRequest } from "next/server";
import { authConfig, SESSION_COOKIE, validSession } from "./lib/auth";
import catalog from "../../../content/catalog.json";
const legacyRoutes = new Map(Object.entries(catalog.documents).map(([source, metadata]) => [
`/docs/${source}`, `/docs/${metadata.slug}`,
]));
export function proxy(request: NextRequest) {
const path = request.nextUrl.pathname;
@@ -49,6 +54,13 @@ export function proxy(request: NextRequest) {
);
}
}
// Keep legacy URLs behind authentication and out of static generation.
const destination = legacyRoutes.get(path);
if (destination && ["GET", "HEAD"].includes(request.method)) {
const target = request.nextUrl.clone();
target.pathname = destination;
return NextResponse.redirect(target, { status: 308, headers });
}
return NextResponse.next({ headers });
}
if (path.startsWith("/api/") || !["GET", "HEAD"].includes(request.method)) {
+68
View File
@@ -1313,3 +1313,71 @@ svg { flex-shrink: 0; }
*, *::before, *::after { animation: none !important; transition: none !important; }
.featured-link:hover, .featured-link:hover .collection-art, .pagination > a:hover { transform: none; }
}
/* Compact navigation and local font roles, with no font downloads. */
:root {
--font-reading: "Charter", "Iowan Old Style", "Songti SC", "STSong", "Noto Serif CJK SC", serif;
--font-code: "SFMono-Regular", Consolas, "Liberation Mono", "PingFang SC", monospace;
}
.top-nav { margin-left: 0; }
.sidebar { padding: 22px 16px 32px; }
.collection-identity { font-size: 14px; gap: 9px; margin: 0 8px 20px; }
.collection-identity svg { width: 18px; height: 18px; }
.nav-group { margin-top: 20px; }
.nav-group > p { margin: 0 8px 6px; font-size: 11px; letter-spacing: .02em; color: #7d8981; }
.nav-item { font-size: 13px; padding: 5px 8px; margin: 0; line-height: 1.5; gap: 6px; }
.nav-item[aria-current="page"]::before { top: 7px; bottom: 7px; }
.collection-nav-link { padding: 7px 8px; margin-bottom: 2px; gap: 9px; font-size: 13px; }
.prose { font-family: var(--font-reading); font-size: 16px; line-height: 1.95; }
.prose :is(h1,h2,h3,h4), .prose table, .code-toolbar, .mobile-toc { font-family: var(--font-sans); }
.prose :is(h1,h2,h3) { font-weight: 600; letter-spacing: -.025em; }
.prose :is(code,pre), .code-block pre, kbd { font-family: var(--font-code); font-variant-ligatures: none; }
.prose code { font-size: .86em; }
@media (min-width: 1201px) {
.home-layout { grid-template-columns: 240px minmax(0,1fr); }
.reading-layout { grid-template-columns: 240px minmax(0,1fr) 222px; }
}
@media (min-width: 761px) and (max-width: 1200px) {
.home-layout, .reading-layout { grid-template-columns: 224px minmax(0,1fr); }
}
@media (max-width: 760px) {
.top-nav { overflow: visible; }
.mobile-navigation .nav-group { margin-top: 16px; }
}
/* Categories stay visible across the header, each with its own disclosure panel. */
.top-nav.category-navigation { position: relative; display: flex; margin-left: 0; gap: 0; }
.category-navigation > ul { display: flex; align-items: stretch; gap: 22px; height: 100%; list-style: none; padding: 0; margin: 0; }
.category-navigation > ul > li { display: flex; align-items: stretch; }
.category-menu { position: relative; }
.category-navigation .category-home, .category-trigger { display: flex; align-items: center; gap: 6px; color: var(--muted); font-family: var(--font-sans); font-size: 13px; white-space: nowrap; }
.category-trigger { position: relative; padding: 0; background: transparent; cursor: pointer; }
.category-trigger > svg { transform: rotate(90deg); transition: transform 160ms; }
.category-trigger[aria-expanded="true"] > svg { transform: rotate(-90deg); }
.category-trigger:hover, .category-trigger[aria-expanded="true"], .category-trigger[data-active="true"] { color: var(--ink); }
.category-trigger[data-active="true"]::after { content: ""; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: var(--green); }
.category-trigger:focus-visible, .category-navigation a:focus-visible { outline: 2px solid var(--green); outline-offset: 4px; border-radius: 3px; }
.category-panel { position: absolute; top: 100%; left: -16px; width: 320px; padding: 12px; background: #fdfefd; border: 1px solid var(--line); border-radius: 0 0 10px 10px; box-shadow: 0 16px 40px #24352b1a; z-index: 50; }
.category-panel[hidden] { display: none; }
.category-menu:last-child .category-panel { left: auto; right: -16px; }
.category-panel-title { margin: 3px 10px 8px; font-size: 11px; color: var(--muted); }
.top-nav .category-panel a { display: flex; align-items: flex-start; gap: 12px; padding: 12px 10px; white-space: normal; border-radius: 6px; color: var(--ink); }
.top-nav .category-panel a::after { display: none; }
.category-panel a:hover, .category-panel a[aria-current] { background: var(--accent-wash); }
.category-panel a > svg { margin-top: 2px; color: var(--green); }
.category-panel strong { display: block; font-size: 13px; font-weight: 550; line-height: 1.5; }
.category-panel small { display: block; margin-top: 4px; font-size: 12px; line-height: 1.6; color: var(--muted); }
@media (min-width: 761px) and (max-width: 1100px) {
.site-header { gap: 20px; }
.category-navigation > ul { gap: 16px; }
.category-navigation .category-home, .category-trigger { font-size: 12px; }
.search-trigger { width: 36px; height: 34px; padding: 8px; justify-content: center; }
.search-trigger span, .search-trigger kbd { display: none; }
}
@media (max-width: 760px) {
.top-nav.category-navigation { overflow: visible; }
.category-navigation > ul { width: 100%; gap: 18px; }
.category-navigation .category-home, .category-trigger { font-size: 12px; }
.category-menu { position: static; }
.category-panel, .category-menu:last-child .category-panel { width: auto; left: 0; right: 0; }
}
+111 -21
View File
@@ -21,90 +21,180 @@
"group": "架构设计",
"description": "Endor 的全球入口、区域运行时与对象数据。",
"draft": true,
"featured": true
"featured": true,
"slug": "endor/endor-concepts-draft",
"category": "Endor"
},
"01-getting-started": {
"group": "开始使用",
"description": "本地开发、文档编写与发布。"
"description": "本地开发、文档编写与发布。",
"slug": "endor/getting-started",
"category": "Endor"
},
"02-mounting-draft": {
"group": "架构设计",
"description": "节点、对象与统一接入协议。",
"draft": true
"draft": true,
"slug": "endor/mounting-draft",
"category": "Endor"
},
"03-figures": {
"group": "图形档案",
"description": "Python 源码与可编辑 SVG 的制图工作流。"
"description": "Python 源码与可编辑 SVG 的制图工作流。",
"slug": "endor/figures",
"category": "Endor"
},
"04-endor-purpose-draft": {
"group": "项目讨论",
"description": "从程序入口走向用户与 AI 的对象操作。",
"draft": true
"draft": true,
"slug": "endor/endor-purpose-draft",
"category": "Endor"
},
"05-object-graphs-gc": {
"collection": "对象关系与垃圾回收",
"group": "运行时基础",
"description": "从对象引用环到标记清除、分代回收与跨代引用,记录内存管理讨论。"
"description": "从对象引用环到标记清除、分代回收与跨代引用,记录内存管理讨论。",
"slug": "language-and-runtime/object-relations/object-graphs-gc",
"category": "语言与运行时"
},
"pretext/00-overview": {
"description": "理解高性能文本布局的原理,沿着 12 章教程阅读源码。",
"featured": true
"featured": true,
"slug": "source-studies/pretext/overview",
"category": "源码研究"
},
"language-design/00-overview": {
"description": "从名称与作用域到反射与表示,按 271 项议题逐步讨论语言设计。",
"featured": true
"featured": true,
"slug": "language-and-runtime/language-design/overview",
"category": "语言与运行时"
},
"language-design/02-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/names-and-scope",
"category": "语言与运行时"
},
"language-design/03-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/values-and-types",
"category": "语言与运行时"
},
"language-design/04-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/storage-and-lifetimes",
"category": "语言与运行时"
},
"language-design/05-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/objects-and-members",
"category": "语言与运行时"
},
"language-design/06-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/expressions-and-operators",
"category": "语言与运行时"
},
"language-design/07-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/functions-and-calls",
"category": "语言与运行时"
},
"language-design/08-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/control-flow",
"category": "语言与运行时"
},
"language-design/09-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/collections-and-access",
"category": "语言与运行时"
},
"language-design/10-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/errors-and-resources",
"category": "语言与运行时"
},
"language-design/11-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/modules-and-execution",
"category": "语言与运行时"
},
"language-design/12-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/concurrency-and-async",
"category": "语言与运行时"
},
"language-design/13-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/external-interaction",
"category": "语言与运行时"
},
"language-design/14-questions": {
"description": "语言设计讨论底稿;包含问题、验收要求和当前确认状态。",
"draft": true
"draft": true,
"slug": "language-and-runtime/language-design/reflection-and-representation",
"category": "语言与运行时"
},
"pretext/01-chapter": {
"slug": "source-studies/pretext/text-measurement",
"category": "源码研究"
},
"pretext/02-chapter": {
"slug": "source-studies/pretext/prepare-and-layout",
"category": "源码研究"
},
"pretext/03-chapter": {
"slug": "source-studies/pretext/text-segmentation",
"category": "源码研究"
},
"pretext/04-chapter": {
"slug": "source-studies/pretext/measurement-cache",
"category": "源码研究"
},
"pretext/05-chapter": {
"slug": "source-studies/pretext/line-breaking",
"category": "源码研究"
},
"pretext/06-chapter": {
"slug": "source-studies/pretext/height-only-layout",
"category": "源码研究"
},
"pretext/07-chapter": {
"slug": "source-studies/pretext/long-text",
"category": "源码研究"
},
"pretext/08-chapter": {
"slug": "source-studies/pretext/accuracy",
"category": "源码研究"
},
"pretext/09-chapter": {
"slug": "source-studies/pretext/virtual-lists",
"category": "源码研究"
},
"pretext/10-chapter": {
"slug": "source-studies/pretext/performance-verification",
"category": "源码研究"
},
"pretext/11-chapter": {
"slug": "source-studies/pretext/source-reading",
"category": "源码研究"
},
"pretext/12-chapter": {
"slug": "source-studies/pretext/applying-the-design",
"category": "源码研究"
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ pnpm dev
## 添加文档
在 `content/docs` 下添加 Markdown 文件,例如 `04-runtime.md`。文件名使用小写字母、数字和连字符;第一个一级标题是导航标题。该文件对应 `/docs/04-runtime`。
在 `content/docs` 下添加 Markdown 文件,例如 `04-runtime.md`。文件名使用小写字母、数字和连字符;第一个一级标题是导航标题。在 `content/catalog.json` 的 `documents` 中以源文件路径登记 `slug` 和 `category`,例如 `slug: "endor/runtime"`、`category: "Endor"`。公开地址为 `/docs/endor/runtime`;源文件编号仅用于排序,不进入公开 URL。既有源文件地址保留永久跳转,修改已有公开 slug 时需另行保留旧地址映射。
开发模式下刷新页面读取修改;本地生产预览需要重新构建。