Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b98592da2 | |||
| 96dcbccbf6 | |||
| d11b30f620 | |||
| a4c9349fce | |||
| 4c4471c2bd | |||
| 53aef1eb64 | |||
| 18af3802c6 | |||
| 4e9ff80828 | |||
| 0e1f99639f |
@@ -196,15 +196,20 @@ async function main() {
|
|||||||
const searchResponse = await request("/api/search", { headers });
|
const searchResponse = await request("/api/search", { headers });
|
||||||
assert.equal(searchResponse.status, 200);
|
assert.equal(searchResponse.status, 200);
|
||||||
const searchIndex = await searchResponse.json();
|
const searchIndex = await searchResponse.json();
|
||||||
|
assert.ok(!searchIndex.some((doc: { collection: string }) => doc.collection === "Endor 文档"), "Retired Endor documents stay out of search");
|
||||||
assert.ok(searchIndex.some((doc: { slug: string; searchText: string }) => doc.slug === "source-studies/pretext/measurement-cache" && 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 }[]) {
|
for (const doc of searchIndex as { slug: string; sourceSlug: string }[]) {
|
||||||
assert.ok(!doc.slug.split("/").some((part) => /^\d+-/.test(part)), doc.slug);
|
assert.ok(!doc.slug.split("/").some((part) => /^\d+-/.test(part)), doc.slug);
|
||||||
const legacy = await request(`/docs/${doc.sourceSlug}?from=legacy`, { headers });
|
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"));
|
assert.ok(legacy.headers.get("cache-control")?.includes("no-store"));
|
||||||
|
if (doc.sourceSlug === doc.slug) {
|
||||||
|
assert.equal(legacy.status, 200, doc.sourceSlug);
|
||||||
|
} else {
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const routes = ["/", ...searchIndex.map((doc: { slug: string }) => `/docs/${doc.slug}`)];
|
const routes = ["/", ...searchIndex.map((doc: { slug: string }) => `/docs/${doc.slug}`)];
|
||||||
const raw = await request("/api/documents/source?slug=endor/endor-purpose-draft", { headers });
|
const raw = await request("/api/documents/source?slug=endor/endor-purpose-draft", { headers });
|
||||||
@@ -215,12 +220,13 @@ async function main() {
|
|||||||
const sourceSlug = "ci-live-document";
|
const sourceSlug = "ci-live-document";
|
||||||
const previous = readDocument(sourceSlug);
|
const previous = readDocument(sourceSlug);
|
||||||
if (command === "exercise") {
|
if (command === "exercise") {
|
||||||
const first = saveDocument({ slug: "endor/ci-live-document", sourceSlug,
|
// Publication is tested in a current collection; Endor is intentionally retired.
|
||||||
title: "Runtime document", category: "Endor", collection: "Endor 文档", group: "CI",
|
const first = saveDocument({ slug: "runtime-tests/ci-live-document", sourceSlug,
|
||||||
|
title: "Runtime document", category: "语言与运行时", collection: "运行时验证", group: "CI",
|
||||||
description: "Isolated smoke test", draft: true, featured: false,
|
description: "Isolated smoke test", draft: true, featured: false,
|
||||||
content: "# Runtime document\n\nInitial body" }, previous?.revision ?? 0);
|
content: "# Runtime document\n\nInitial body" }, previous?.revision ?? 0);
|
||||||
assert.equal((await request(`/docs/${first.slug}`, { headers })).status, 200);
|
assert.equal((await request(`/docs/${first.slug}`, { headers })).status, 200);
|
||||||
saveDocument({ ...first, slug: "endor/ci-live-document-renamed", content: "# Runtime document\n\nUpdated without rebuilding" }, first.revision);
|
saveDocument({ ...first, slug: "runtime-tests/ci-live-document-renamed", content: "# Runtime document\n\nUpdated without rebuilding" }, first.revision);
|
||||||
}
|
}
|
||||||
const stored = readDocument(sourceSlug);
|
const stored = readDocument(sourceSlug);
|
||||||
assert.ok(stored, "Smoke document must exist after creation or restart");
|
assert.ok(stored, "Smoke document must exist after creation or restart");
|
||||||
@@ -228,11 +234,11 @@ async function main() {
|
|||||||
const live = await request(`/docs/${stored.slug}`, { headers });
|
const live = await request(`/docs/${stored.slug}`, { headers });
|
||||||
assert.equal(live.status, 200);
|
assert.equal(live.status, 200);
|
||||||
assert.ok((await live.text()).includes("Updated without rebuilding"));
|
assert.ok((await live.text()).includes("Updated without rebuilding"));
|
||||||
const alias = await request("/docs/endor/ci-live-document?live=1", { headers });
|
const alias = await request("/docs/runtime-tests/ci-live-document?live=1", { headers });
|
||||||
assert.equal(alias.status, 308);
|
assert.equal(alias.status, 308);
|
||||||
assert.ok(alias.headers.get("location")?.includes("ci-live-document-renamed?live=1"));
|
assert.ok(alias.headers.get("location")?.includes("ci-live-document-renamed?live=1"));
|
||||||
const updatedIndex = await (await request("/api/search", { headers })).json();
|
const updatedIndex = await (await request("/api/search", { headers })).json();
|
||||||
assert.ok(updatedIndex.some((doc: { searchText: string }) => doc.searchText.includes("Updated without rebuilding")));
|
assert.ok(updatedIndex.some((doc: { slug: string; searchText: string }) => doc.slug === stored.slug && doc.searchText.includes("Updated without rebuilding")));
|
||||||
}
|
}
|
||||||
let home = "";
|
let home = "";
|
||||||
let purpose = "";
|
let purpose = "";
|
||||||
@@ -254,6 +260,17 @@ async function main() {
|
|||||||
for (const id of ids) assert.ok(html.includes(`href="#${id}"`), `TOC anchor: ${route}#${id}`);
|
for (const id of ids) assert.ok(html.includes(`href="#${id}"`), `TOC anchor: ${route}#${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assert.ok(!home.includes('data-category="Endor"'), "Superseded Endor collection is absent from navigation");
|
||||||
|
if (searchIndex.some((doc: { collection: string }) => doc.collection === "Endorphin")) {
|
||||||
|
assert.equal([...home.matchAll(/data-category="([^"]+)"/g)][0]?.[1], "Endorphin", "Endorphin is the first category");
|
||||||
|
assert.ok(home.includes('/docs/endorphin/value-proposition'), "Endorphin opens at its value proposition");
|
||||||
|
}
|
||||||
|
// Retired documents remain reachable for historical links and gallery checks.
|
||||||
|
if (!purpose) {
|
||||||
|
const archived = await request("/docs/endor/endor-purpose-draft", { headers });
|
||||||
|
assert.equal(archived.status, 200);
|
||||||
|
purpose = await archived.text();
|
||||||
|
}
|
||||||
assert.ok(home.includes("资料库"));
|
assert.ok(home.includes("资料库"));
|
||||||
assert.ok(home.includes("/docs/source-studies/pretext/overview"));
|
assert.ok(home.includes("/docs/source-studies/pretext/overview"));
|
||||||
// Inline preview and zoom dialog must both use the compressed browser asset.
|
// Inline preview and zoom dialog must both use the compressed browser asset.
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DocLink } from "@/lib/docs";
|
import type { DocLink } from "@/lib/docs";
|
||||||
|
|
||||||
|
// Keep superseded Endor pages addressable without offering them as current material.
|
||||||
|
export function catalogDocuments<T extends DocLink>(docs: T[]): T[] {
|
||||||
|
return docs.filter((doc) => doc.collection !== "Endor 文档")
|
||||||
|
.sort((a, b) => Number(b.collection === "Endorphin") - Number(a.collection === "Endorphin"));
|
||||||
|
}
|
||||||
|
|
||||||
export function collectionLabel(name: string) {
|
export function collectionLabel(name: string) {
|
||||||
return name.replace(/ 文档$| 源码教程$|问题集$/g, "");
|
return name.replace(/ 文档$| 源码教程$|问题集$/g, "");
|
||||||
}
|
}
|
||||||
@@ -15,10 +21,13 @@ export function collectionIcon(name?: string): "book" | "code" | "layers" | "nod
|
|||||||
if (name?.includes("垃圾回收")) return "nodes";
|
if (name?.includes("垃圾回收")) return "nodes";
|
||||||
return "book";
|
return "book";
|
||||||
}
|
}
|
||||||
export function collectionEntries(docs: DocLink[]) {
|
export function collectionEntries(documents: DocLink[]) {
|
||||||
|
const docs = catalogDocuments(documents);
|
||||||
return [...new Set(docs.map((doc) => doc.collection))].map((name) => {
|
return [...new Set(docs.map((doc) => doc.collection))].map((name) => {
|
||||||
const items = docs.filter((doc) => doc.collection === name);
|
const items = docs.filter((doc) => doc.collection === name);
|
||||||
const entry = items.find((doc) => doc.slug.endsWith("/overview")) ?? items[0];
|
const entry = items.find((doc) => doc.slug.endsWith("/overview"))
|
||||||
|
?? (name === "Endorphin" ? items.find((doc) => doc.featured) : undefined)
|
||||||
|
?? items[0];
|
||||||
return { name, entry, count: items.length };
|
return { name, entry, count: items.length };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { randomUUID, createHash } from "node:crypto";
|
import { randomUUID, createHash } from "node:crypto";
|
||||||
|
import { catalogDocuments } from "./collections";
|
||||||
import { openSiteDatabase } from "./site-database";
|
import { openSiteDatabase } from "./site-database";
|
||||||
import { DocumentValidationError, DocumentConflictError } from "./document-errors";
|
import { DocumentValidationError, DocumentConflictError } from "./document-errors";
|
||||||
import { sectionSchema, loadSections, reconcileSections, persistSections, splitSections, outline,
|
import { sectionSchema, loadSections, reconcileSections, persistSections, splitSections, outline,
|
||||||
@@ -188,7 +189,7 @@ export async function readDoc(slug: string): Promise<Doc> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function listDocs(): Promise<DocLink[]> {
|
export async function listDocs(): Promise<DocLink[]> {
|
||||||
return allDocuments().map(({ content: _, ...doc }) => { void _; return doc; });
|
return catalogDocuments(allDocuments()).map(({ content: _, ...doc }) => { void _; return doc; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit, atomic bootstrap only. Runtime reads never import repository data.
|
// Explicit, atomic bootstrap only. Runtime reads never import repository data.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { catalogDocuments } from "@/lib/collections";
|
||||||
import { allDocuments } from "@/lib/docs";
|
import { allDocuments } from "@/lib/docs";
|
||||||
|
|
||||||
// The proxy authenticates this endpoint, including index content.
|
// The proxy authenticates this endpoint, including index content.
|
||||||
@@ -9,7 +10,7 @@ export default async function handler(request: NextApiRequest, response: NextApi
|
|||||||
return response.status(405).end();
|
return response.status(405).end();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const index = allDocuments().map(({ content, ...doc }) => ({ ...doc, searchText: content }));
|
const index = catalogDocuments(allDocuments()).map(({ content, ...doc }) => ({ ...doc, searchText: content }));
|
||||||
return response.status(200).json(index);
|
return response.status(200).json(index);
|
||||||
} catch {
|
} catch {
|
||||||
return response.status(503).json({ error: "文档数据库暂时不可用" });
|
return response.status(503).json({ error: "文档数据库暂时不可用" });
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export default function DocPage({
|
|||||||
const [active, setActive] = useState("");
|
const [active, setActive] = useState("");
|
||||||
|
|
||||||
const siblings = docs.filter((d) => d.collection === doc.collection);
|
const siblings = docs.filter((d) => d.collection === doc.collection);
|
||||||
const collectionEntry = collectionEntries(docs).find((c) => c.name === doc.collection)!.entry;
|
// Historical pages remain readable even when their collection is outside the catalog.
|
||||||
|
const collectionEntry = collectionEntries(docs).find((c) => c.name === doc.collection)?.entry ?? doc;
|
||||||
const index = siblings.findIndex((d) => d.slug === doc.slug);
|
const index = siblings.findIndex((d) => d.slug === doc.slug);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
|
|||||||
Reference in New Issue
Block a user