@eqtylab/docs 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -12
- package/dist/{chunk-VJN3HXKM.js → chunk-LNQV7TVH.js} +18 -6
- package/dist/chunk-LNQV7TVH.js.map +1 -0
- package/dist/chunk-OM7745ZQ.js +77 -0
- package/dist/chunk-OM7745ZQ.js.map +1 -0
- package/dist/{chunk-IWKZQ4CW.js → chunk-VNQZJMDO.js} +4 -2
- package/dist/chunk-VNQZJMDO.js.map +1 -0
- package/dist/{chunk-K7PJRTK3.js → chunk-X4LT7ASQ.js} +31 -4
- package/dist/chunk-X4LT7ASQ.js.map +1 -0
- package/dist/config.d.ts +10 -1
- package/dist/config.js +1 -1
- package/dist/index.js +297 -28
- package/dist/index.js.map +1 -1
- package/dist/internal/extract-versions.d.ts +33 -0
- package/dist/internal/rehype-base-url.d.ts +9 -2
- package/dist/internal/rehype-base-url.js +1 -1
- package/dist/internal/remark-archive-document.d.ts +6 -0
- package/dist/loaders.d.ts +5 -5
- package/dist/loaders.js +18 -7
- package/dist/loaders.js.map +1 -1
- package/dist/nav.d.ts +4 -0
- package/dist/nav.js +21 -7
- package/dist/nav.js.map +1 -1
- package/dist/paths.d.ts +25 -3
- package/dist/paths.js +1 -1
- package/dist/runtime/chrome/GlobalSearch.tsx +7 -2
- package/dist/runtime/chrome/Header.astro +9 -6
- package/dist/runtime/chrome/NavDrawer.astro +44 -1
- package/dist/runtime/chrome/NavTree.astro +1 -11
- package/dist/runtime/chrome/Prose.astro +18 -4
- package/dist/runtime/chrome/VersionBanner.astro +19 -0
- package/dist/runtime/chrome/VersionSwitcher.tsx +105 -0
- package/dist/runtime/components/CodeFence.astro +1 -1
- package/dist/runtime/layouts/DocsPage.astro +36 -4
- package/dist/runtime/layouts/DocsShell.astro +14 -3
- package/dist/runtime/layouts/RedirectPage.astro +19 -0
- package/dist/runtime/lib/nav-data.ts +44 -15
- package/dist/runtime/lib/versions-ui.ts +12 -0
- package/dist/runtime/routes/docs-md.ts +50 -11
- package/dist/runtime/routes/docs.astro +182 -73
- package/dist/runtime/routes/llms-txt.ts +2 -1
- package/dist/runtime/routes/not-found.astro +27 -0
- package/dist/runtime/styles/prose.css +9 -2
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -1
- package/dist/versions.d.ts +37 -0
- package/package.json +2 -2
- package/dist/chunk-GSSSQTEV.js +0 -51
- package/dist/chunk-GSSSQTEV.js.map +0 -1
- package/dist/chunk-IWKZQ4CW.js.map +0 -1
- package/dist/chunk-K7PJRTK3.js.map +0 -1
- package/dist/chunk-VJN3HXKM.js.map +0 -1
|
@@ -1,42 +1,64 @@
|
|
|
1
1
|
/** Bridges the content collections into the pure nav builder. Needs `astro:content`, so it lives in the runtime tree. */
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
breadcrumbsFor,
|
|
4
|
+
buildNavTree,
|
|
5
|
+
buildTocTree,
|
|
6
|
+
flattenNav,
|
|
7
|
+
prevNextFor,
|
|
8
|
+
} from '@eqtylab/docs/nav';
|
|
3
9
|
import type { GroupConfig } from '@eqtylab/docs/nav';
|
|
10
|
+
import { docsHref } from '@eqtylab/docs/paths';
|
|
4
11
|
import type { DocsNavEntry, NavNode } from '@eqtylab/docs/types';
|
|
5
12
|
import { getCollection } from 'astro:content';
|
|
6
13
|
import CONFIG from 'virtual:eqty-docs/config';
|
|
7
14
|
|
|
8
15
|
export { buildTocTree, breadcrumbsFor, prevNextFor };
|
|
9
16
|
|
|
10
|
-
/**
|
|
11
|
-
export function pathContext() {
|
|
17
|
+
/** The version is a route segment after the path prefix; latest has none. */
|
|
18
|
+
export function pathContext(versionId?: string) {
|
|
12
19
|
return {
|
|
13
20
|
base: import.meta.env.BASE_URL,
|
|
14
21
|
pathPrefix: CONFIG.pathPrefix,
|
|
22
|
+
versionPrefix: versionId,
|
|
15
23
|
};
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
/**
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
/** Collection names for a version id, or for latest when undefined. */
|
|
27
|
+
export function collectionsFor(versionId?: string): { docs: string; groups: string } {
|
|
28
|
+
if (!versionId) return { docs: 'docs', groups: 'docsGroups' };
|
|
29
|
+
const entry = CONFIG.versionManifest.find((v) => v.id === versionId);
|
|
30
|
+
if (!entry) throw new Error(`[@eqtylab/docs] unknown version id "${versionId}"`);
|
|
31
|
+
return { docs: `docs_${entry.suffix}`, groups: `docsGroups_${entry.suffix}` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `getCollection` is typed against the generated DataEntryMap, which cannot know names built at runtime.
|
|
35
|
+
const anyCollection = getCollection as unknown as (
|
|
36
|
+
name: string,
|
|
37
|
+
filter?: (entry: { data: Record<string, unknown> }) => boolean
|
|
38
|
+
) => Promise<never[]>;
|
|
39
|
+
|
|
40
|
+
/** All entries of a version, with drafts filtered out unless we're in `astro dev`. */
|
|
41
|
+
export async function docsEntries(versionId?: string) {
|
|
42
|
+
return anyCollection(collectionsFor(versionId).docs, ({ data }) =>
|
|
43
|
+
import.meta.env.DEV ? true : !(data as { draft?: boolean }).draft
|
|
22
44
|
);
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
async function groupMap(): Promise<Map<string, GroupConfig>> {
|
|
47
|
+
async function groupMap(versionId?: string): Promise<Map<string, GroupConfig>> {
|
|
26
48
|
const map = new Map<string, GroupConfig>();
|
|
27
49
|
try {
|
|
28
|
-
const groups = await
|
|
29
|
-
for (const group of groups) {
|
|
30
|
-
map.set(group.id, group.data
|
|
50
|
+
const groups = await anyCollection(collectionsFor(versionId).groups);
|
|
51
|
+
for (const group of groups as Array<{ id: string; data: GroupConfig }>) {
|
|
52
|
+
map.set(group.id, group.data);
|
|
31
53
|
}
|
|
32
54
|
} catch {
|
|
33
|
-
//
|
|
55
|
+
// The groups collection is optional; no _group.yaml means alphabetical ordering.
|
|
34
56
|
}
|
|
35
57
|
return map;
|
|
36
58
|
}
|
|
37
59
|
|
|
38
|
-
export async function docsNav(currentPath: string): Promise<NavNode[]> {
|
|
39
|
-
const [entries, groups] = await Promise.all([docsEntries(), groupMap()]);
|
|
60
|
+
export async function docsNav(currentPath: string, versionId?: string): Promise<NavNode[]> {
|
|
61
|
+
const [entries, groups] = await Promise.all([docsEntries(versionId), groupMap(versionId)]);
|
|
40
62
|
|
|
41
63
|
const navEntries: DocsNavEntry[] = entries.map((entry) => ({
|
|
42
64
|
id: entry.id,
|
|
@@ -52,9 +74,10 @@ export async function docsNav(currentPath: string): Promise<NavNode[]> {
|
|
|
52
74
|
entries: navEntries,
|
|
53
75
|
groups,
|
|
54
76
|
currentPath,
|
|
55
|
-
paths: pathContext(),
|
|
77
|
+
paths: pathContext(versionId),
|
|
56
78
|
defaultCollapsed: CONFIG.sidebar.collapsed,
|
|
57
79
|
defaultSort: CONFIG.sidebar.sort,
|
|
80
|
+
defaultIndexLabel: CONFIG.sidebar.indexLabel,
|
|
58
81
|
extra: (CONFIG.sidebar.extra ?? []) as NavNode[],
|
|
59
82
|
onWarn: warnOnce,
|
|
60
83
|
});
|
|
@@ -72,3 +95,9 @@ function deprecationBadge(deprecated: unknown) {
|
|
|
72
95
|
if (!deprecated) return undefined;
|
|
73
96
|
return { text: 'Deprecated', variant: 'warning' as const };
|
|
74
97
|
}
|
|
98
|
+
|
|
99
|
+
/** The first linkable page of a version. Version roots and switcher fallbacks land here. */
|
|
100
|
+
export async function firstNavHref(versionId?: string): Promise<string> {
|
|
101
|
+
const tree = await docsNav('/', versionId);
|
|
102
|
+
return flattenNav(tree)[0]?.href ?? docsHref('', pathContext(versionId));
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Shapes the chrome receives. Computed at build in the route; the components hold no logic. */
|
|
2
|
+
export interface SwitcherItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
current: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SwitcherData {
|
|
9
|
+
latest: SwitcherItem;
|
|
10
|
+
/** One group per major, newest first; items newest first. */
|
|
11
|
+
groups: Array<{ label: string; items: SwitcherItem[] }>;
|
|
12
|
+
}
|
|
@@ -4,30 +4,69 @@ import type { APIRoute } from 'astro';
|
|
|
4
4
|
import { getCollection } from 'astro:content';
|
|
5
5
|
import CONFIG from 'virtual:eqty-docs/config';
|
|
6
6
|
|
|
7
|
+
import { collectionsFor } from '../lib/nav-data.ts';
|
|
8
|
+
|
|
7
9
|
export async function getStaticPaths() {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const visible = ({ data }: { data: { draft?: boolean; noIndex?: boolean } }) =>
|
|
11
|
+
!data.draft && !data.noIndex;
|
|
12
|
+
const anyCollection = getCollection as unknown as (
|
|
13
|
+
name: string,
|
|
14
|
+
f: typeof visible
|
|
15
|
+
) => Promise<never[]>;
|
|
16
|
+
const versions: Array<string | undefined> = [
|
|
17
|
+
undefined,
|
|
18
|
+
...CONFIG.versionManifest.map((v) => v.id),
|
|
19
|
+
];
|
|
20
|
+
const out: Array<{ params: { slug: string }; props: { entry: unknown } }> = [];
|
|
21
|
+
for (const versionId of versions) {
|
|
22
|
+
const entries = (await anyCollection(collectionsFor(versionId).docs, visible)) as Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
}>;
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
// An empty slug would emit a file literally called ".md".
|
|
27
|
+
const page = idToPath(entry.id) || 'index';
|
|
28
|
+
out.push({ params: { slug: versionId ? `${versionId}/${page}` : page }, props: { entry } });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Deprecated = boolean | { message?: string; replacedBy?: string } | undefined;
|
|
35
|
+
|
|
36
|
+
/** Agents read the twin, not the rendered banner, so the deprecation has to travel with it. */
|
|
37
|
+
function deprecationNotice(deprecated: Deprecated) {
|
|
38
|
+
if (!deprecated) return [];
|
|
39
|
+
const detail = typeof deprecated === 'object' ? deprecated : {};
|
|
40
|
+
const message = stripHtml(detail.message) ?? 'This page documents a deprecated feature.';
|
|
41
|
+
const replacedBy = detail.replacedBy ? ` Use ${stripHtml(detail.replacedBy)} instead.` : '';
|
|
42
|
+
return [`> **Deprecated** — ${message}${replacedBy}`, ''];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Frontmatter messages may carry authored HTML links, which are noise in plain Markdown. */
|
|
46
|
+
function stripHtml(value?: string) {
|
|
47
|
+
return value?.replace(/<[^>]*>/g, '').trim() || undefined;
|
|
17
48
|
}
|
|
18
49
|
|
|
19
50
|
export const GET: APIRoute = ({ props }) => {
|
|
20
51
|
const { entry } = props as {
|
|
21
|
-
entry: {
|
|
52
|
+
entry: {
|
|
53
|
+
body?: string;
|
|
54
|
+
data: { title: string; description?: string; deprecated?: Deprecated };
|
|
55
|
+
};
|
|
22
56
|
};
|
|
23
57
|
|
|
24
58
|
const frontmatter = ['---', `title: ${JSON.stringify(entry.data.title)}`];
|
|
25
59
|
if (entry.data.description) {
|
|
26
60
|
frontmatter.push(`description: ${JSON.stringify(entry.data.description)}`);
|
|
27
61
|
}
|
|
62
|
+
if (entry.data.deprecated) {
|
|
63
|
+
frontmatter.push('deprecated: true');
|
|
64
|
+
}
|
|
28
65
|
frontmatter.push(`source: ${JSON.stringify(CONFIG.title)}`, '---', '');
|
|
29
66
|
|
|
30
|
-
|
|
67
|
+
const body = [...deprecationNotice(entry.data.deprecated), entry.body ?? ''].join('\n');
|
|
68
|
+
|
|
69
|
+
return new Response(frontmatter.join('\n') + body, {
|
|
31
70
|
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
32
71
|
});
|
|
33
72
|
};
|
|
@@ -1,92 +1,201 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { render } from 'astro:content';
|
|
3
3
|
import CONFIG from 'virtual:eqty-docs/config';
|
|
4
4
|
import { docsHref, idToPath, joinPath, normalizePath } from '@eqtylab/docs/paths';
|
|
5
5
|
import DocsPage from '../layouts/DocsPage.astro';
|
|
6
|
-
import
|
|
6
|
+
import RedirectPage from '../layouts/RedirectPage.astro';
|
|
7
|
+
import {
|
|
8
|
+
breadcrumbsFor,
|
|
9
|
+
buildTocTree,
|
|
10
|
+
docsEntries,
|
|
11
|
+
docsNav,
|
|
12
|
+
firstNavHref,
|
|
13
|
+
pathContext,
|
|
14
|
+
prevNextFor,
|
|
15
|
+
} from '../lib/nav-data.ts';
|
|
7
16
|
import { firstParagraph } from '../lib/summary.ts';
|
|
8
17
|
import { mdxComponents } from '../lib/mdx-components.ts';
|
|
18
|
+
import type { SwitcherData } from '../lib/versions-ui.ts';
|
|
9
19
|
import PageFooter from '../chrome/PageFooter.astro';
|
|
10
20
|
import Prose from '../chrome/Prose.astro';
|
|
11
21
|
|
|
22
|
+
type Entry = Awaited<ReturnType<typeof docsEntries>>[number] & {
|
|
23
|
+
id: string;
|
|
24
|
+
body?: string;
|
|
25
|
+
filePath?: string;
|
|
26
|
+
data: Record<string, any>;
|
|
27
|
+
};
|
|
28
|
+
|
|
12
29
|
export async function getStaticPaths() {
|
|
13
|
-
const
|
|
14
|
-
|
|
30
|
+
const owned = new Set((CONFIG.ownedByConsumer ?? []).map(normalizePath));
|
|
31
|
+
// Astro does not de-duplicate injected routes against consumer pages. Latest only: a consumer cannot own a versioned path.
|
|
32
|
+
const latest = ((await docsEntries()) as Entry[]).filter(
|
|
33
|
+
(entry) => !owned.has(normalizePath(docsHref(entry.id, pathContext())))
|
|
34
|
+
);
|
|
35
|
+
const copies = await Promise.all(
|
|
36
|
+
CONFIG.versionManifest.map(async (v) => ({ v, entries: (await docsEntries(v.id)) as Entry[] }))
|
|
15
37
|
);
|
|
16
38
|
|
|
17
|
-
const
|
|
39
|
+
const idsOf = new Map<string | undefined, Set<string>>([
|
|
40
|
+
[undefined, new Set(latest.map((e) => e.id))],
|
|
41
|
+
]);
|
|
42
|
+
for (const { v, entries } of copies) idsOf.set(v.id, new Set(entries.map((e) => e.id)));
|
|
43
|
+
const roots = new Map<string | undefined, string>();
|
|
44
|
+
for (const v of CONFIG.versionManifest) roots.set(v.id, await firstNavHref(v.id));
|
|
45
|
+
|
|
46
|
+
const latestLabel = CONFIG.currentVersion ? CONFIG.currentVersion.id : 'latest';
|
|
47
|
+
const hrefFor = (versionId: string | undefined, id: string) =>
|
|
48
|
+
idsOf.get(versionId)?.has(id)
|
|
49
|
+
? docsHref(id, pathContext(versionId))
|
|
50
|
+
: versionId
|
|
51
|
+
? docsHref('', pathContext(versionId))
|
|
52
|
+
: docsHref('', pathContext());
|
|
18
53
|
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
.
|
|
54
|
+
const switcherFor = (versionId: string | undefined, id: string): SwitcherData | undefined => {
|
|
55
|
+
// Nothing to switch to until a release is tagged: a picker offering only the page you are
|
|
56
|
+
// already on is noise, and it displaces the header's control cluster.
|
|
57
|
+
if (!CONFIG.currentVersion || !CONFIG.versionManifest.length) return undefined;
|
|
58
|
+
const byMajor = new Map<string, SwitcherData['groups'][number]>();
|
|
59
|
+
for (const v of CONFIG.versionManifest) {
|
|
60
|
+
const major = `v${v.group.split('.')[0]}`;
|
|
61
|
+
if (!byMajor.has(major)) byMajor.set(major, { label: major, items: [] });
|
|
62
|
+
byMajor
|
|
63
|
+
.get(major)!
|
|
64
|
+
.items.push({ label: v.id, href: hrefFor(v.id, id), current: v.id === versionId });
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
latest: { label: latestLabel, href: hrefFor(undefined, id), current: versionId === undefined },
|
|
68
|
+
groups: [...byMajor.values()],
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
...latest.map((entry) => ({
|
|
23
74
|
// `undefined` (not '') is how a rest param matches the empty path.
|
|
24
75
|
params: { slug: idToPath(entry.id) || undefined },
|
|
25
|
-
props: { entry },
|
|
26
|
-
}))
|
|
76
|
+
props: { entry, switcher: switcherFor(undefined, entry.id) },
|
|
77
|
+
})),
|
|
78
|
+
...copies.flatMap(({ v, entries }) => [
|
|
79
|
+
...entries.map((entry) => ({
|
|
80
|
+
params: { slug: `${v.id}/${idToPath(entry.id)}`.replace(/\/$/, '') },
|
|
81
|
+
props: {
|
|
82
|
+
entry,
|
|
83
|
+
version: v.id,
|
|
84
|
+
switcher: switcherFor(v.id, entry.id),
|
|
85
|
+
canonical: idsOf.get(undefined)!.has(entry.id)
|
|
86
|
+
? docsHref(entry.id, pathContext())
|
|
87
|
+
: undefined,
|
|
88
|
+
latestHref: hrefFor(undefined, entry.id),
|
|
89
|
+
latestLabel,
|
|
90
|
+
},
|
|
91
|
+
})),
|
|
92
|
+
// 3.9.1 had no index.mdx, so without this the version root and every stub pointing at it would 404.
|
|
93
|
+
...(idsOf.get(v.id)!.has('index')
|
|
94
|
+
? []
|
|
95
|
+
: [{ params: { slug: v.id }, props: { redirectTo: roots.get(v.id)!, version: v.id } }]),
|
|
96
|
+
]),
|
|
97
|
+
// Every target goes through docsHref so base and pathPrefix apply.
|
|
98
|
+
...CONFIG.versionRedirects.map((r) => ({
|
|
99
|
+
params: { slug: r.id },
|
|
100
|
+
props: {
|
|
101
|
+
redirectTo: r.to
|
|
102
|
+
? (roots.get(r.to) ?? docsHref('', pathContext(r.to)))
|
|
103
|
+
: docsHref('', pathContext()),
|
|
104
|
+
version: r.to,
|
|
105
|
+
},
|
|
106
|
+
})),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface Props {
|
|
111
|
+
entry?: Entry;
|
|
112
|
+
version?: string | null;
|
|
113
|
+
switcher?: SwitcherData;
|
|
114
|
+
canonical?: string;
|
|
115
|
+
latestHref?: string;
|
|
116
|
+
latestLabel?: string;
|
|
117
|
+
redirectTo?: string;
|
|
27
118
|
}
|
|
28
119
|
|
|
29
|
-
const { entry } = Astro.props;
|
|
30
|
-
const { Content, headings } = await render(entry);
|
|
31
|
-
|
|
32
|
-
const tocConfig = entry.data.tableOfContents ?? CONFIG.tableOfContents;
|
|
33
|
-
const showToc = tocConfig !== false;
|
|
34
|
-
const toc = showToc
|
|
35
|
-
? buildTocTree(headings, {
|
|
36
|
-
minLevel: (tocConfig as { minLevel?: number })?.minLevel ?? 2,
|
|
37
|
-
maxLevel: (tocConfig as { maxLevel?: number })?.maxLevel ?? 3,
|
|
38
|
-
})
|
|
39
|
-
: [];
|
|
40
|
-
|
|
41
|
-
const nav = await docsNav(Astro.url.pathname);
|
|
42
|
-
const splash = entry.data.template === 'splash';
|
|
43
|
-
|
|
44
|
-
// Trail is [group, ..., page]; a length of one means the page has no section above it.
|
|
45
|
-
const trail = breadcrumbsFor(nav, Astro.url.pathname);
|
|
46
|
-
const crumbs = trail.length > 1 ? trail.map((node) => node.label).join(' › ') : undefined;
|
|
47
|
-
|
|
48
|
-
// Ancestors only. `crumbs` above keeps the page as well, because a search result row
|
|
49
|
-
// needs the page name to identify itself. The two diverge on purpose.
|
|
50
|
-
const renderedTrail = trail.slice(0, -1).map((node) => ({ label: node.label, href: node.href }));
|
|
51
|
-
|
|
52
|
-
// Indexed only, never rendered: a page with no `description` still needs a result summary.
|
|
53
|
-
const fallbackSummary = entry.data.description ? undefined : firstParagraph(entry.body);
|
|
54
|
-
|
|
55
|
-
const { prev, next } = CONFIG.footer.showPrevNext ? prevNextFor(nav, Astro.url.pathname) : {};
|
|
56
|
-
|
|
57
|
-
// `editUrl` is a base; the content path is appended. The slice is taken at the
|
|
58
|
-
// consumer's own content directory, which is configurable.
|
|
59
|
-
const contentPath = entry.filePath?.split(`${CONFIG.contentDir}/`).pop();
|
|
60
|
-
const editHref =
|
|
61
|
-
CONFIG.footer.editUrl && contentPath
|
|
62
|
-
? `${CONFIG.footer.editUrl.replace(/\/$/, '')}/${contentPath}`
|
|
63
|
-
: undefined;
|
|
64
|
-
|
|
65
|
-
// File-shaped (/x/label.md) beside the directory-shaped page; 'index' must match docs-md.ts.
|
|
66
|
-
const markdownHref = CONFIG.routing.markdownTwins
|
|
67
|
-
? `${joinPath(import.meta.env.BASE_URL, CONFIG.pathPrefix, idToPath(entry.id) || 'index')}.md`
|
|
68
|
-
: undefined;
|
|
120
|
+
const { entry, version, switcher, canonical, latestHref, latestLabel, redirectTo } = Astro.props;
|
|
69
121
|
---
|
|
70
122
|
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
{redirectTo && <RedirectPage to={redirectTo} version={version ?? null} />}
|
|
124
|
+
{
|
|
125
|
+
!redirectTo &&
|
|
126
|
+
entry &&
|
|
127
|
+
(async () => {
|
|
128
|
+
const { Content, headings } = await render(entry as never);
|
|
129
|
+
|
|
130
|
+
const tocConfig = entry.data.tableOfContents ?? CONFIG.tableOfContents;
|
|
131
|
+
const showToc = tocConfig !== false;
|
|
132
|
+
const toc = showToc
|
|
133
|
+
? buildTocTree(headings, {
|
|
134
|
+
minLevel: (tocConfig as { minLevel?: number })?.minLevel ?? 2,
|
|
135
|
+
maxLevel: (tocConfig as { maxLevel?: number })?.maxLevel ?? 3,
|
|
136
|
+
})
|
|
137
|
+
: [];
|
|
138
|
+
|
|
139
|
+
const nav = await docsNav(Astro.url.pathname, version ?? undefined);
|
|
140
|
+
const splash = entry.data.template === 'splash';
|
|
141
|
+
|
|
142
|
+
const trail = breadcrumbsFor(nav, Astro.url.pathname);
|
|
143
|
+
const crumbs = trail.length > 1 ? trail.map((node) => node.label).join(' › ') : undefined;
|
|
144
|
+
|
|
145
|
+
// Ancestors only. `crumbs` above keeps the page as well, because a search result row
|
|
146
|
+
// needs the page name to identify itself. The two diverge on purpose.
|
|
147
|
+
const renderedTrail = trail.slice(0, -1).map((node) => ({ label: node.label, href: node.href }));
|
|
148
|
+
|
|
149
|
+
// Indexed only, never rendered: a page with no `description` still needs a result summary.
|
|
150
|
+
const fallbackSummary = entry.data.description ? undefined : firstParagraph(entry.body);
|
|
151
|
+
|
|
152
|
+
const { prev, next } = CONFIG.footer.showPrevNext ? prevNextFor(nav, Astro.url.pathname) : {};
|
|
153
|
+
|
|
154
|
+
// Old versions are read-only and their files live in the cache, so the split below would
|
|
155
|
+
// return the whole cache path and produce a bogus link. Latest only.
|
|
156
|
+
const contentPath = version ? undefined : entry.filePath?.split(`${CONFIG.contentDir}/`).pop();
|
|
157
|
+
const editHref =
|
|
158
|
+
CONFIG.footer.editUrl && contentPath
|
|
159
|
+
? `${CONFIG.footer.editUrl.replace(/\/$/, '')}/${contentPath}`
|
|
160
|
+
: undefined;
|
|
161
|
+
|
|
162
|
+
// File-shaped (/v3.9/x/label.md) beside the directory-shaped page; 'index' must match docs-md.ts.
|
|
163
|
+
const markdownHref = CONFIG.routing.markdownTwins
|
|
164
|
+
? `${joinPath(import.meta.env.BASE_URL, CONFIG.pathPrefix, version ?? undefined, idToPath(entry.id) || 'index')}.md`
|
|
165
|
+
: undefined;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<DocsPage
|
|
169
|
+
title={entry.data.title}
|
|
170
|
+
description={entry.data.description}
|
|
171
|
+
nav={nav}
|
|
172
|
+
toc={toc}
|
|
173
|
+
showToc={showToc}
|
|
174
|
+
splash={splash}
|
|
175
|
+
version={version ?? undefined}
|
|
176
|
+
switcher={switcher}
|
|
177
|
+
noIndex={Boolean(version)}
|
|
178
|
+
canonical={canonical}
|
|
179
|
+
latestHref={latestHref}
|
|
180
|
+
latestLabel={latestLabel}
|
|
181
|
+
>
|
|
182
|
+
<Prose
|
|
183
|
+
title={entry.data.title}
|
|
184
|
+
description={entry.data.description}
|
|
185
|
+
deprecated={entry.data.deprecated}
|
|
186
|
+
markdownHref={markdownHref}
|
|
187
|
+
source={entry.body}
|
|
188
|
+
crumbs={crumbs}
|
|
189
|
+
trail={renderedTrail}
|
|
190
|
+
fallbackSummary={fallbackSummary}
|
|
191
|
+
version={version ?? undefined}
|
|
192
|
+
>
|
|
193
|
+
<Content components={mdxComponents} />
|
|
194
|
+
</Prose>
|
|
195
|
+
{!splash && (
|
|
196
|
+
<PageFooter prev={prev} next={next} editHref={editHref} text={CONFIG.footer.text} />
|
|
197
|
+
)}
|
|
198
|
+
</DocsPage>
|
|
199
|
+
);
|
|
200
|
+
})()
|
|
201
|
+
}
|
|
@@ -26,8 +26,9 @@ export const GET: APIRoute = async (ctx) => {
|
|
|
26
26
|
'',
|
|
27
27
|
...sorted.map((entry) => {
|
|
28
28
|
const href = docsHref(entry.id, paths).replace(/\/$/, '');
|
|
29
|
+
const prefix = entry.data.deprecated ? ' (deprecated)' : '';
|
|
29
30
|
const suffix = entry.data.description ? `: ${entry.data.description}` : '';
|
|
30
|
-
return `- [${entry.data.title}](${origin}${href}.md)${suffix}`;
|
|
31
|
+
return `- [${entry.data.title}](${origin}${href}.md)${prefix}${suffix}`;
|
|
31
32
|
}),
|
|
32
33
|
'',
|
|
33
34
|
];
|
|
@@ -8,9 +8,36 @@ import NotFoundBody from '../chrome/NotFoundBody.tsx';
|
|
|
8
8
|
const homeHref = withBase(CONFIG.pathPrefix ? `/${CONFIG.pathPrefix}/` : '/', {
|
|
9
9
|
base: import.meta.env.BASE_URL,
|
|
10
10
|
});
|
|
11
|
+
|
|
12
|
+
// One static file serves every miss, so the version has to be read from the URL in the browser.
|
|
13
|
+
// The tables ride in a JSON script rather than `define:vars`, which would force `is:inline` and
|
|
14
|
+
// a second copy of the matching logic that nothing tests.
|
|
15
|
+
const fallback = {
|
|
16
|
+
base: import.meta.env.BASE_URL,
|
|
17
|
+
pathPrefix: CONFIG.pathPrefix,
|
|
18
|
+
copyIds: CONFIG.versionManifest.map((v) => v.id),
|
|
19
|
+
redirects: CONFIG.versionRedirects,
|
|
20
|
+
};
|
|
11
21
|
---
|
|
12
22
|
|
|
13
23
|
<DocsShell title="Page not found" noIndex>
|
|
14
24
|
<Header />
|
|
25
|
+
<script
|
|
26
|
+
type="application/json"
|
|
27
|
+
data-eq-docs-fallback
|
|
28
|
+
is:inline
|
|
29
|
+
set:html={JSON.stringify(fallback)}
|
|
30
|
+
/>
|
|
15
31
|
<NotFoundBody homeHref={homeHref} client:load />
|
|
16
32
|
</DocsShell>
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
import { resolveVersionedPath } from '@eqtylab/docs/paths';
|
|
36
|
+
|
|
37
|
+
const source = document.querySelector('[data-eq-docs-fallback]')?.textContent;
|
|
38
|
+
if (source) {
|
|
39
|
+
const target = resolveVersionedPath(location.pathname, JSON.parse(source));
|
|
40
|
+
// A reader who typed a real release URL should reach the page, not the 404 body.
|
|
41
|
+
if (target && target !== location.pathname) location.replace(target);
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
@@ -88,9 +88,16 @@
|
|
|
88
88
|
@apply text-text-primary hover:text-lilac-400 font-medium underline underline-offset-4 transition-all;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/*
|
|
91
|
+
/*
|
|
92
|
+
Inline code, but never the contents of a highlighted block.
|
|
93
|
+
|
|
94
|
+
`font-mono` is not decoration: Equality's preflight reaches --font-mono through
|
|
95
|
+
`var()`, and that variable is declared `@theme inline`, so Tailwind emits it only
|
|
96
|
+
where a utility references it. The utility inlines the literal stack instead, which
|
|
97
|
+
is what keeps code monospaced in a consumer that uses it nowhere else.
|
|
98
|
+
*/
|
|
92
99
|
:not(pre) > &:is(code) {
|
|
93
|
-
@apply dark:bg-background-overlay bg-greyscale-200 text-text-secondary rounded-sm p-1 text-sm;
|
|
100
|
+
@apply dark:bg-background-overlay bg-greyscale-200 text-text-secondary rounded-sm p-1 font-mono text-sm;
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
/* No table rules: markdown tables render through Equality's Table. */
|
package/dist/schema.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ export declare function groupSchema(): z.ZodObject<{
|
|
|
69
69
|
label: z.ZodOptional<z.ZodString>;
|
|
70
70
|
icon: z.ZodOptional<z.ZodString>;
|
|
71
71
|
order: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
72
|
+
indexLabel: z.ZodOptional<z.ZodString>;
|
|
72
73
|
sort: z.ZodOptional<z.ZodEnum<{
|
|
73
74
|
alpha: "alpha";
|
|
74
75
|
filename: "filename";
|
package/dist/schema.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Pure. Turns a tag list into the copies to build and the release URLs that redirect to them. */
|
|
2
|
+
export type Granularity = 'major' | 'minor' | 'patch';
|
|
3
|
+
export interface ParsedTag {
|
|
4
|
+
tag: string;
|
|
5
|
+
major: number;
|
|
6
|
+
minor: number;
|
|
7
|
+
patch: number;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
export interface VersionCopy {
|
|
11
|
+
id: string;
|
|
12
|
+
suffix: string;
|
|
13
|
+
group: string;
|
|
14
|
+
tag: string;
|
|
15
|
+
}
|
|
16
|
+
/** `to: null` means latest, i.e. `/`. */
|
|
17
|
+
export interface VersionRedirect {
|
|
18
|
+
id: string;
|
|
19
|
+
to: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface VersionSelection {
|
|
22
|
+
/** Newest first. */
|
|
23
|
+
copies: VersionCopy[];
|
|
24
|
+
redirects: VersionRedirect[];
|
|
25
|
+
/** Tags that did not parse as MAJOR.MINOR.PATCH, prereleases included. */
|
|
26
|
+
skipped: string[];
|
|
27
|
+
/** Parsed tags above `current`. */
|
|
28
|
+
above: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function parseTag(tag: string): ParsedTag | null;
|
|
31
|
+
export declare function compareTags(a: ParsedTag, b: ParsedTag): number;
|
|
32
|
+
export declare function groupOf(v: ParsedTag, granularity: Granularity): string;
|
|
33
|
+
export declare function idOf(group: string): string;
|
|
34
|
+
/** Collection names are identifiers, so `v3.9` becomes `v3_9`. */
|
|
35
|
+
export declare function suffixOf(id: string): string;
|
|
36
|
+
export declare function highestTag(tags: string[]): ParsedTag | null;
|
|
37
|
+
export declare function selectVersions(tags: string[], current: string, granularity: Granularity, skippedGroups?: string[]): VersionSelection;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eqtylab/docs",
|
|
3
3
|
"description": "Astro documentation framework built on the Equality design system",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://equality.eqtylab.io/",
|
|
7
7
|
"repository": {
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"dependencies": {
|
|
100
100
|
"@tailwindcss/vite": "^4.1.16",
|
|
101
101
|
"github-slugger": "^2.0.0",
|
|
102
|
-
"pagefind": "
|
|
102
|
+
"pagefind": "1.5.2",
|
|
103
103
|
"tailwindcss": "^4.1.16",
|
|
104
104
|
"tinyglobby": "^0.2.15",
|
|
105
105
|
"unist-util-visit": "^5.1.0",
|
package/dist/chunk-GSSSQTEV.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { visit } from 'unist-util-visit';
|
|
2
|
-
|
|
3
|
-
// src/internal/rehype-base-url.ts
|
|
4
|
-
var URL_ATTRS = ["href", "src", "poster"];
|
|
5
|
-
var SRCSET_ATTRS = ["srcSet", "srcset"];
|
|
6
|
-
function shouldRewrite(value, base) {
|
|
7
|
-
if (!value.startsWith("/")) return false;
|
|
8
|
-
if (value.startsWith("//")) return false;
|
|
9
|
-
if (base !== "/" && (value === base.replace(/\/$/, "") || value.startsWith(base))) return false;
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
function rewrite(value, base) {
|
|
13
|
-
return shouldRewrite(value, base) ? base.replace(/\/$/, "") + value : value;
|
|
14
|
-
}
|
|
15
|
-
function rewriteSrcset(value, base) {
|
|
16
|
-
return value.split(",").map((candidate) => {
|
|
17
|
-
const trimmed = candidate.trim();
|
|
18
|
-
if (!trimmed) return candidate;
|
|
19
|
-
const [url, ...descriptors] = trimmed.split(/\s+/);
|
|
20
|
-
return [rewrite(url, base), ...descriptors].join(" ");
|
|
21
|
-
}).join(", ");
|
|
22
|
-
}
|
|
23
|
-
function rehypeBaseUrl(options) {
|
|
24
|
-
const base = options.base || "/";
|
|
25
|
-
return function transformer(tree) {
|
|
26
|
-
if (base === "/") return;
|
|
27
|
-
visit(tree, "element", (node) => {
|
|
28
|
-
const props = node.properties;
|
|
29
|
-
if (!props) return;
|
|
30
|
-
for (const attr of URL_ATTRS) {
|
|
31
|
-
const value = props[attr];
|
|
32
|
-
if (typeof value === "string") props[attr] = rewrite(value, base);
|
|
33
|
-
}
|
|
34
|
-
for (const attr of SRCSET_ATTRS) {
|
|
35
|
-
const value = props[attr];
|
|
36
|
-
if (typeof value === "string") props[attr] = rewriteSrcset(value, base);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function rewriteHtmlBase(html, base) {
|
|
42
|
-
if (!base || base === "/") return html;
|
|
43
|
-
return html.replace(
|
|
44
|
-
/\b(href|src)=("|')(\/(?!\/)[^"']*)\2/g,
|
|
45
|
-
(_match, attr, quote, url) => `${attr}=${quote}${rewrite(url, base)}${quote}`
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export { rehypeBaseUrl, rewriteHtmlBase };
|
|
50
|
-
//# sourceMappingURL=chunk-GSSSQTEV.js.map
|
|
51
|
-
//# sourceMappingURL=chunk-GSSSQTEV.js.map
|