feat: add protocol explorer with catalogs and live playground

This commit is contained in:
2026-10-19 18:21:30 +09:00
parent b82c1d0a70
commit 75a36f2298
38 changed files with 1836 additions and 83 deletions
+30 -7
View File
@@ -5,12 +5,15 @@ import test from 'node:test';
import { createMarkdownProcessor } from '@astrojs/markdown-remark';
import { load } from 'cheerio';
import documentLinks, { contentsRoot } from '../plugins/document-links.mjs';
import { bookTree } from '../src/lib/landing-tree.ts';
import { publicTree } from '../src/lib/landing-tree.ts';
import { createWorldspaceData } from '../src/lib/worldspace-data.ts';
const pages = {
'/': { title: 'Worldspace Interface Protocol', href: '/', html: '<h1>Home</h1>' },
'/book': { title: 'Book', href: '/book/', html: '<h1>Book</h1>' },
'/sources': { title: 'Sources', href: '/sources/', html: '<h1>Sources</h1>' },
'/packages': { title: 'Packages', href: '/packages/', html: '<h1>Packages</h1>' },
'/playground': { title: 'Playground', href: '/playground/', html: '<h1>Playground</h1>' },
};
const source = (id) => ({ id, title: id, href: `/book/${id}/`, html: `<h1>${id}</h1>` });
@@ -49,15 +52,15 @@ function checkIdentity(data) {
test('actual public documents keep Web and display identities but acquire real parent paths', () => {
const data = createWorldspaceData(documents, pages);
checkIdentity(data);
const display = bookTree(documents);
assert.equal(data.nodes.length, documents.length + 2);
const display = publicTree(documents);
assert.equal(data.nodes.length, documents.length + 5);
assert.deepEqual(data.nodes.map((node) => node.displayPath), display.map((node) => node.path));
for (const node of data.nodes) {
const legacy = display.find((item) => item.path === node.displayPath);
assert.equal(node.parent, legacy.parent === undefined ? undefined : data.nodes.find((item) => item.displayPath === legacy.parent).path);
if (legacy.href) {
assert.equal(node.href, legacy.href);
assert.equal(node.html, documents.find((entry) => `/${entry.id}.md` === node.displayPath).html);
assert.equal(node.html, (documents.find((entry) => `/${entry.id}.md` === node.displayPath) ?? pages[node.displayPath]).html);
assert.match(node.html, /<h1/);
}
}
@@ -66,7 +69,20 @@ test('actual public documents keep Web and display identities but acquire real p
assert.equal(section.parent, '/book/1-concept.md');
assert.equal(section.href, '/book/1.1-interaction-abstraction/');
assert.equal(data.nodes[1].name, 'book');
assert.equal(display[1].name, 'book/');
assert.equal(display[1].name, 'book');
});
test('catalog and playground objects retain root identity, standalone URLs and no static descendants', () => {
const data = createWorldspaceData(documents, pages);
assert.deepEqual(data.nodes.filter((node) => node.parent === '/').map((node) => node.path), ['/book', '/sources', '/packages', '/playground']);
for (const path of ['/sources', '/packages', '/playground']) {
const leaf = data.nodes.find((node) => node.path === path);
assert.equal(leaf.displayPath, path);
assert.equal(leaf.href, `${path}/`);
assert.equal(leaf.html, pages[path].html);
assert.equal(data.nodes.filter((node) => node.parent === path).length, 0);
assert.ok(!data.nodes.some((node) => node.path.startsWith(`${path}/`)));
}
});
test('drafts are removed before hierarchy, body and URL generation', () => {
@@ -86,6 +102,9 @@ test('nested numbering, real directories and site base prefixes remain unambiguo
const data = createWorldspaceData(docs, {
'/': { ...pages['/'], href: '/reference/' },
'/book': { ...pages['/book'], href: '/reference/book/' },
'/sources': { ...pages['/sources'], href: '/reference/sources/' },
'/packages': { ...pages['/packages'], href: '/reference/packages/' },
'/playground': { ...pages['/playground'], href: '/reference/playground/' },
'/other': { title: 'other/', href: '/reference/book/other/', html: '<h1>other/</h1>' },
});
checkIdentity(data);
@@ -108,11 +127,14 @@ test('invalid, duplicate and incomplete generated inputs fail rather than lose d
assert.throws(() => createWorldspaceData([source('dir/a')], pages), /Missing rendered page: \/dir/);
});
test('root and book index content is supplied by their existing page renderers', () => {
test('root, book, catalog and playground content is supplied by their shared page renderers', () => {
const data = createWorldspaceData([], pages);
assert.deepEqual(data.nodes, [
{ path: '/', name: '', displayPath: '/', ...pages['/'] },
{ path: '/book', name: 'book', displayPath: '/book', parent: '/', ...pages['/book'] },
{ path: '/sources', name: 'sources', displayPath: '/sources', parent: '/', ...pages['/sources'] },
{ path: '/packages', name: 'packages', displayPath: '/packages', parent: '/', ...pages['/packages'] },
{ path: '/playground', name: 'playground', displayPath: '/playground', parent: '/', ...pages['/playground'] },
]);
});
@@ -122,10 +144,11 @@ test('built artifact contains exactly public routes and the same rendered articl
{ skip: process.env.WIP_TEST_GENERATED !== '1' }, async () => {
const data = JSON.parse(await readFile(new URL('../dist/worldspace.json', import.meta.url), 'utf8'));
checkIdentity(data);
assert.equal(data.nodes.length, documents.length + 2);
assert.equal(data.nodes.length, documents.length + 5);
assert.deepEqual(new Set(data.nodes.filter((node) => node.displayPath.endsWith('.md')).map((node) => node.displayPath.slice(1))), new Set(publicFiles));
for (const node of data.nodes) {
const relative = node.displayPath === '/' ? 'index.html' : node.displayPath === '/book' ? 'book/index.html'
: ['/sources', '/packages', '/playground'].includes(node.displayPath) ? `${node.displayPath.slice(1)}/index.html`
: `book/${node.displayPath.slice(1, -3)}/index.html`;
const page = load(await readFile(new URL(`../dist/${relative}`, import.meta.url), 'utf8'));
// Astro adds hydration/render markers outside the actual article fragment.