@jaamd/jaad 0.11.0 → 1.0.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 +8 -2
- package/dist/jaad.css +232 -347
- package/docs/00-overview.md +25 -0
- package/docs/01-getting-started/01-installation.md +216 -0
- package/docs/01-getting-started/02-pages.md +109 -0
- package/docs/01-getting-started/03-deployment.md +100 -0
- package/docs/02-configurations/01-reference.md +215 -0
- package/docs/02-configurations/02-frontmatter.md +105 -0
- package/docs/02-configurations/03-i18n.md +210 -0
- package/docs/02-configurations/04-search.md +58 -0
- package/docs/02-configurations/05-themes.md +104 -0
- package/docs/02-configurations/06-styles.md +156 -0
- package/docs/02-configurations/07-custom-pages.md +119 -0
- package/docs/02-configurations/08-advanced-layout.md +139 -0
- package/docs/03-upgrading.md +208 -0
- package/index.ts +8 -22
- package/package.json +10 -9
- package/site.ts +18 -1
- package/src/config.ts +25 -176
- package/src/{advanced.ts → docs/advanced.ts} +6 -4
- package/src/docs/collection.ts +109 -0
- package/src/docs/components/Breadcrumbs.astro +29 -0
- package/src/docs/components/DocsMobileNav.astro +90 -0
- package/src/docs/components/DocsNavTree.astro +57 -0
- package/src/docs/components/DocsPage.astro +66 -0
- package/src/{components/docs → docs/components}/DocsPageNav.astro +4 -4
- package/src/docs/components/DocsSidebarLeft.astro +53 -0
- package/src/{components/docs → docs/components}/PageActions.astro +10 -8
- package/src/{components/docs → docs/components}/StructuredData.astro +3 -3
- package/src/{components/docs → docs/components}/TableOfContents.astro +20 -22
- package/src/docs/config.ts +167 -0
- package/src/docs/content.ts +36 -0
- package/src/docs/endpoints.ts +89 -0
- package/src/docs/integration.ts +249 -0
- package/src/{layouts → docs/layouts}/DefaultDocsFrame.astro +7 -5
- package/src/{layouts → docs/layouts}/Docs.astro +44 -43
- package/src/docs/layouts/Page.astro +18 -0
- package/src/docs/link-plugins.ts +52 -0
- package/src/docs/links.ts +42 -0
- package/src/{locale-tree.ts → docs/locale-tree.ts} +1 -1
- package/src/docs/model/ids.ts +87 -0
- package/src/docs/model/rendered.ts +185 -0
- package/src/docs/model/tree.ts +124 -0
- package/src/docs/model/validate.ts +75 -0
- package/src/docs/navigation.ts +75 -0
- package/src/{routes → docs/routes}/404.astro +1 -1
- package/src/docs/routes/docs-index.astro +19 -0
- package/src/docs/routes/docs-slug.astro +54 -0
- package/src/{routes → docs/routes}/docs-slug.md.ts +8 -10
- package/src/{routes → docs/routes}/llms-locale.txt.ts +1 -1
- package/src/{routes → docs/routes}/llms.txt.ts +1 -1
- package/src/{routes → docs/routes}/search-index-locale.json.ts +1 -1
- package/src/{routes → docs/routes}/search-index.json.ts +1 -1
- package/src/docs/runtime.ts +17 -0
- package/src/docs/search-index.ts +39 -0
- package/src/docs/styles/components/breadcrumbs.css +26 -0
- package/src/docs/styles/components/docs-mobile-nav.css +47 -0
- package/src/docs/styles/components/docs-page-nav.css +41 -0
- package/src/docs/styles/components/docs-sidebar.css +59 -0
- package/src/docs/styles/components/page-actions.css +51 -0
- package/src/docs/styles/components/toc.css +38 -0
- package/src/docs/styles/components.css +7 -0
- package/src/docs/styles/docs.css +81 -0
- package/src/{@types/docs.ts → docs/types.ts} +8 -16
- package/src/{urls.ts → docs/urls.ts} +2 -16
- package/src/env.d.ts +1 -1
- package/src/integration.ts +15 -249
- package/src/{components → site/components}/Footer.astro +1 -1
- package/src/{components → site/components}/Header.astro +14 -11
- package/src/{components → site/components}/LocaleSwitcher.astro +2 -2
- package/src/{components → site/components}/NavMobile.astro +1 -1
- package/src/{components → site/components}/SearchPalette.astro +160 -79
- package/src/{components → site/components}/ThemeSwitcher.astro +1 -1
- package/src/site/config.ts +149 -0
- package/src/{infer.ts → site/infer.ts} +3 -3
- package/src/site/integration.ts +59 -0
- package/src/{layouts/Page.astro → site/layouts/Shell.astro} +6 -3
- package/src/{locales.ts → site/locales.ts} +9 -1
- package/src/{docs-config.ts → site/runtime.ts} +1 -12
- package/src/site/search.ts +227 -0
- package/src/{social.ts → site/social.ts} +2 -2
- package/src/site/styles/components/header.css +31 -0
- package/src/{styles → site/styles}/components/search.css +21 -1
- package/src/{styles → site/styles}/components/shell.css +4 -0
- package/src/site/styles/components.css +10 -0
- package/src/{styles → site/styles}/global.css +4 -26
- package/src/{themes/index.ts → site/themes.ts} +1 -1
- package/src/{ui → site/ui}/en.ts +3 -5
- package/src/{ui → site/ui}/it.ts +2 -4
- package/src/site/urls.ts +14 -0
- package/src/{virtual.ts → site/virtual.ts} +12 -18
- package/src/styles.css +5 -0
- package/src/collection.ts +0 -97
- package/src/components/ScrollToTop.astro +0 -43
- package/src/components/docs/Breadcrumbs.astro +0 -36
- package/src/components/docs/DocsMobileNav.astro +0 -93
- package/src/components/docs/DocsPage.astro +0 -68
- package/src/components/docs/DocsSidebarLeft.astro +0 -82
- package/src/content.ts +0 -42
- package/src/navigation.ts +0 -97
- package/src/routes/docs-index.astro +0 -19
- package/src/routes/docs-slug.astro +0 -58
- package/src/styles/components/breadcrumbs.css +0 -13
- package/src/styles/components/docs-mobile-nav.css +0 -9
- package/src/styles/components/docs-page-nav.css +0 -31
- package/src/styles/components/docs-sidebar.css +0 -42
- package/src/styles/components/header.css +0 -20
- package/src/styles/components/page-actions.css +0 -13
- package/src/styles/components/scroll-to-top.css +0 -11
- package/src/styles/components/toc.css +0 -21
- package/src/styles/docs.css +0 -112
- package/src/utils/docs.ts +0 -300
- package/src/utils/endpoints.ts +0 -80
- package/src/utils/search.ts +0 -83
- /package/src/{styles → docs/styles}/components/docs-page.css +0 -0
- /package/src/{styles → docs/styles}/jaamd.css +0 -0
- /package/src/{components → site/components}/Head.astro +0 -0
- /package/src/{components → site/components}/Nav.astro +0 -0
- /package/src/{components → site/components}/SocialLinks.astro +0 -0
- /package/src/{components → site/components}/locale-switcher.ts +0 -0
- /package/src/{fonts → site/fonts}/OFL.txt +0 -0
- /package/src/{fonts → site/fonts}/inter.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather-italic.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather.woff2 +0 -0
- /package/src/{fonts.ts → site/fonts.ts} +0 -0
- /package/src/{forges.ts → site/forges.ts} +0 -0
- /package/src/{utils → site}/helpers.ts +0 -0
- /package/src/{styles → site/styles}/components/footer.css +0 -0
- /package/src/{styles → site/styles}/components/locale-switcher.css +0 -0
- /package/src/{styles → site/styles}/components/nav-mobile.css +0 -0
- /package/src/{styles → site/styles}/components/nav.css +0 -0
- /package/src/{styles → site/styles}/components/not-found.css +0 -0
- /package/src/{styles → site/styles}/components/social-links.css +0 -0
- /package/src/{styles → site/styles}/components/theme-switcher.css +0 -0
- /package/src/{styles → site/styles}/jaamd-forward.css +0 -0
- /package/src/{styles → site/styles}/jaamd-reverse.css +0 -0
- /package/src/{styles → site/styles}/tailwind.css +0 -0
- /package/src/{ui.ts → site/ui.ts} +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsNavGroup } from '../navigation.ts'
|
|
3
|
+
import { ui } from '../../site/runtime.ts'
|
|
4
|
+
import DocsNavTree from './DocsNavTree.astro'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
groups: DocsNavGroup[]
|
|
8
|
+
locale?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { groups, locale } = Astro.props
|
|
12
|
+
const t = ui(locale)
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<aside class="docs-sidebar-left jaad-sidebar">
|
|
16
|
+
<nav aria-label={t('nav.documentation')}>
|
|
17
|
+
<DocsNavTree {groups} />
|
|
18
|
+
</nav>
|
|
19
|
+
</aside>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
const SELECTOR = '.docs-sidebar-left'
|
|
23
|
+
let scrollTop = 0
|
|
24
|
+
|
|
25
|
+
const sidebar = () => document.querySelector<HTMLElement>(SELECTOR)
|
|
26
|
+
|
|
27
|
+
/** Scrolls the sidebar only, never the document, and only when needed. */
|
|
28
|
+
function revealCurrent() {
|
|
29
|
+
const el = sidebar()
|
|
30
|
+
const current = el?.querySelector('[aria-current="page"]')
|
|
31
|
+
if (!el || !current) return
|
|
32
|
+
|
|
33
|
+
const box = el.getBoundingClientRect()
|
|
34
|
+
const item = current.getBoundingClientRect()
|
|
35
|
+
if (item.top >= box.top && item.bottom <= box.bottom) return
|
|
36
|
+
|
|
37
|
+
el.scrollTop += item.top - box.top - (box.height - item.height) / 2
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
document.addEventListener('astro:before-swap', () => {
|
|
41
|
+
scrollTop = sidebar()?.scrollTop ?? 0
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
document.addEventListener('astro:after-swap', () => {
|
|
45
|
+
const el = sidebar()
|
|
46
|
+
if (!el) return
|
|
47
|
+
el.scrollTop = scrollTop
|
|
48
|
+
// A deep link, or a page whose entry sits outside the restored window.
|
|
49
|
+
revealCurrent()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
revealCurrent()
|
|
53
|
+
</script>
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { docsMarkdownHref } from '
|
|
2
|
+
import { editUrl } from '../../site/infer.ts'
|
|
3
|
+
import { ui } from '../../site/runtime.ts'
|
|
4
|
+
import { docsUrls } from '../runtime.ts'
|
|
5
|
+
import { docsMarkdownHref } from '../urls.ts'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/** Where the page's directory is edited, or null to leave the link out. */
|
|
9
|
+
editBase: string | null
|
|
10
|
+
/** The file below that directory, without its extension. */
|
|
11
|
+
editPath: string
|
|
10
12
|
/** URL-safe slug, e.g. "markdown/inner-html". */
|
|
11
13
|
cleanSlug: string
|
|
12
14
|
locale?: string
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
const {
|
|
17
|
+
const { editBase, editPath, cleanSlug, locale } = Astro.props
|
|
16
18
|
const t = ui(locale)
|
|
17
19
|
|
|
18
20
|
const mdHref = docsMarkdownHref(cleanSlug, docsUrls(locale))
|
|
19
|
-
const editHref =
|
|
21
|
+
const editHref = editBase ? editUrl(editBase, `${editPath}.md`) : null
|
|
20
22
|
---
|
|
21
23
|
|
|
22
24
|
<div class="docs-page-actions">
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import config from 'virtual:jaad/config'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { formatChapterTitle } from '../model/ids.ts'
|
|
4
|
+
import { docsUrls } from '../runtime.ts'
|
|
5
|
+
import { docsPageHref } from '../urls.ts'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
title: string
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type {
|
|
3
|
-
import { ui } from '../../
|
|
2
|
+
import type { DocsHeading } from '../types.ts'
|
|
3
|
+
import { ui } from '../../site/runtime.ts'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
|
-
headings:
|
|
6
|
+
headings: DocsHeading[]
|
|
7
7
|
locale?: string
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -12,29 +12,27 @@ const t = ui(locale)
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
{headings.length > 0 && (
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
</nav>
|
|
32
|
-
</aside>
|
|
15
|
+
<nav id="docs-toc" aria-label={t('toc.label')} class="docs-toc">
|
|
16
|
+
<h2 class="docs-toc-title">{t('toc.title')}</h2>
|
|
17
|
+
<ul class="docs-toc-list">
|
|
18
|
+
{headings.map((heading) => (
|
|
19
|
+
<li>
|
|
20
|
+
<a
|
|
21
|
+
href={`#${heading.slug}`}
|
|
22
|
+
data-toc-link
|
|
23
|
+
class:list={['docs-toc-link', heading.depth === 3 && 'docs-toc-link-nested']}
|
|
24
|
+
>
|
|
25
|
+
{heading.text}
|
|
26
|
+
</a>
|
|
27
|
+
</li>
|
|
28
|
+
))}
|
|
29
|
+
</ul>
|
|
30
|
+
</nav>
|
|
33
31
|
)}
|
|
34
32
|
|
|
35
33
|
<script>
|
|
36
34
|
function initToc(): void {
|
|
37
|
-
const sidebar = document.getElementById('toc
|
|
35
|
+
const sidebar = document.getElementById('docs-toc')
|
|
38
36
|
// astro:page-load also fires on the first load, which would observe twice.
|
|
39
37
|
if (!sidebar || sidebar.dataset.tocInit) return
|
|
40
38
|
sidebar.dataset.tocInit = '1'
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { z } from 'astro/zod'
|
|
4
|
+
import { editBaseFrom, type RepoInfo } from '../site/infer.ts'
|
|
5
|
+
import { isLocale } from '../site/locales.ts'
|
|
6
|
+
import { normaliseBasePath } from '../site/urls.ts'
|
|
7
|
+
import { detectLocales, validateLocaleTree } from './locale-tree.ts'
|
|
8
|
+
import { slugToTitle } from './model/ids.ts'
|
|
9
|
+
|
|
10
|
+
/** The options of the documentation: where the markdown is and where it is served. */
|
|
11
|
+
export const docsConfigShape = {
|
|
12
|
+
docsDir: z.string().default('./docs'),
|
|
13
|
+
routeBase: z.string().default('/'),
|
|
14
|
+
/** One directory per section, keyed by its url segment. Replaces `docsDir`. */
|
|
15
|
+
sections: z
|
|
16
|
+
.record(z.string(), z.object({ dir: z.string(), label: z.string().optional() }))
|
|
17
|
+
.superRefine((sections, context) => {
|
|
18
|
+
for (const key of Object.keys(sections)) {
|
|
19
|
+
if (/^[a-z0-9]+(-[a-z0-9]+)*$/.test(key)) continue
|
|
20
|
+
context.addIssue({
|
|
21
|
+
code: 'custom',
|
|
22
|
+
path: [key],
|
|
23
|
+
message: 'a section key is its url segment: lowercase letters, digits and dashes',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.optional(),
|
|
28
|
+
editLink: z.union([z.boolean(), z.string()]).default(true),
|
|
29
|
+
/** Only when detection guesses wrong: pin the set, or false to switch it off. */
|
|
30
|
+
locales: z
|
|
31
|
+
.union([z.array(z.string().refine(isLocale, { message: 'not a locale code' })), z.literal(false)])
|
|
32
|
+
.optional(),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const docsConfigSchema = z.object(docsConfigShape)
|
|
36
|
+
|
|
37
|
+
export type DocsConfig = z.output<typeof docsConfigSchema>
|
|
38
|
+
|
|
39
|
+
/** Where the docs are and where they are served. */
|
|
40
|
+
export interface DocsResolvedConfig {
|
|
41
|
+
/** Where the docs are mounted, without a trailing slash. Empty at the root. */
|
|
42
|
+
base: string
|
|
43
|
+
/** `docsDir` as configured, relative to the project. */
|
|
44
|
+
dir: string
|
|
45
|
+
/** Whether `dir` holds pages: always without sections; with them, only
|
|
46
|
+
* when set, for the pages outside every section. */
|
|
47
|
+
dirInUse: boolean
|
|
48
|
+
/** Empty when the docs are not split into sections. */
|
|
49
|
+
sections: DocsSection[]
|
|
50
|
+
/** Edit url of the pages in `dir`, `:path` standing for the file. */
|
|
51
|
+
editBase: string | null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DocsSection {
|
|
55
|
+
/** The url segment. */
|
|
56
|
+
key: string
|
|
57
|
+
label: string
|
|
58
|
+
/** As configured, relative to the project. */
|
|
59
|
+
dir: string
|
|
60
|
+
editBase: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A directory of markdown, and the section it is when the docs have sections. */
|
|
64
|
+
export interface DocsRoot {
|
|
65
|
+
/** Absolute. */
|
|
66
|
+
dir: string
|
|
67
|
+
section?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Empty at the root, otherwise one leading slash and no trailing one. */
|
|
71
|
+
function resolveEditBase(editLink: DocsConfig['editLink'], repo: RepoInfo | null, docsDir: string): string | null {
|
|
72
|
+
if (typeof editLink === 'string') return editLink
|
|
73
|
+
if (!editLink || !repo) return null
|
|
74
|
+
return editBaseFrom(repo, docsDir)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveSections(config: DocsConfig, repo: RepoInfo | null): DocsSection[] {
|
|
78
|
+
return Object.entries(config.sections ?? {}).map(([key, section]) => ({
|
|
79
|
+
key,
|
|
80
|
+
label: section.label ?? slugToTitle(key),
|
|
81
|
+
dir: section.dir,
|
|
82
|
+
editBase: resolveEditBase(config.editLink, repo, section.dir),
|
|
83
|
+
}))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** No directory may be, or be inside, another. */
|
|
87
|
+
function assertSeparateRoots(roots: DocsRoot[], cwd: string): void {
|
|
88
|
+
const paths = roots.map((root) => root.dir + sep)
|
|
89
|
+
const nested = roots.filter((_, at) => paths.some((other, from) => from !== at && paths[at].startsWith(other)))
|
|
90
|
+
if (nested.length === 0) return
|
|
91
|
+
|
|
92
|
+
throw new Error(
|
|
93
|
+
'jaad: sections overlap\n' +
|
|
94
|
+
nested
|
|
95
|
+
.map((root) => ` ${root.section ?? 'docsDir'}: ${relative(cwd, root.dir)} is, or is inside, another`)
|
|
96
|
+
.join('\n') +
|
|
97
|
+
'\nGive every section, and docsDir, a directory of its own.',
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The docs, and the locale codes their directories are written in.
|
|
103
|
+
* `docsDirSet` tells whether the user set `docsDir`: alongside sections, only
|
|
104
|
+
* then does it hold pages.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveDocsConfig(
|
|
107
|
+
config: DocsConfig,
|
|
108
|
+
lang: string,
|
|
109
|
+
docsDirSet: boolean,
|
|
110
|
+
repo: RepoInfo | null,
|
|
111
|
+
cwd: string,
|
|
112
|
+
): { docs: DocsResolvedConfig; locales: string[] } {
|
|
113
|
+
const sections = resolveSections(config, repo)
|
|
114
|
+
const docs: DocsResolvedConfig = {
|
|
115
|
+
base: normaliseBasePath(config.routeBase),
|
|
116
|
+
dir: config.docsDir,
|
|
117
|
+
dirInUse: sections.length === 0 || docsDirSet,
|
|
118
|
+
sections,
|
|
119
|
+
editBase: resolveEditBase(config.editLink, repo, config.docsDir),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const roots = docsRoots(docs, cwd)
|
|
123
|
+
assertSeparateRoots(roots, cwd)
|
|
124
|
+
const locales = resolveLocales(config, lang, roots)
|
|
125
|
+
|
|
126
|
+
const ambiguous = sections.find((section) => locales.includes(section.key))
|
|
127
|
+
if (ambiguous) {
|
|
128
|
+
throw new Error(`jaad: section "${ambiguous.key}" has the name of a language the docs are written in`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { docs, locales }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Where the markdown lives: `dir` when in use, then the directory of every section. */
|
|
135
|
+
export function docsRoots(docs: Pick<DocsResolvedConfig, 'dir' | 'dirInUse' | 'sections'>, cwd: string): DocsRoot[] {
|
|
136
|
+
return [
|
|
137
|
+
...(docs.dirInUse ? [{ dir: resolve(cwd, docs.dir) }] : []),
|
|
138
|
+
...docs.sections.map((section) => ({ dir: resolve(cwd, section.dir), section: section.key })),
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Directory names win; `locales` only overrides what detection found. */
|
|
143
|
+
function resolveLocales(config: DocsConfig, lang: string, roots: DocsRoot[]): string[] {
|
|
144
|
+
if (config.locales === false) return []
|
|
145
|
+
|
|
146
|
+
const perRoot = roots.map((root) => resolveLocalesIn(root.dir, config, lang))
|
|
147
|
+
const [first, ...rest] = perRoot
|
|
148
|
+
const differing = rest.find((locales) => locales.join() !== first.join())
|
|
149
|
+
if (differing) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
'jaad: sections disagree on their languages\n' +
|
|
152
|
+
` found ${first.join(', ') || 'none'} and ${differing.join(', ') || 'none'}\n` +
|
|
153
|
+
'Give every section the same locale directories, or none.',
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
return first
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveLocalesIn(docsDir: string, config: DocsConfig, lang: string): string[] {
|
|
160
|
+
const locales = Array.isArray(config.locales) ? config.locales : detectLocales(docsDir, lang)
|
|
161
|
+
// A pinned list skips detection, and with it the missing-directory guard;
|
|
162
|
+
// the integration warns about that case with better words than readdir does.
|
|
163
|
+
if (locales.length === 0 || !existsSync(docsDir)) return []
|
|
164
|
+
|
|
165
|
+
validateLocaleTree(docsDir, locales, lang)
|
|
166
|
+
return locales.map((locale) => locale.toLowerCase()).toSorted()
|
|
167
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { frontmatterFields } from '@jaamd/astro/frontmatter'
|
|
2
|
+
import { glob } from 'astro/loaders'
|
|
3
|
+
import { z } from 'astro/zod'
|
|
4
|
+
import { defineCollection } from 'astro:content'
|
|
5
|
+
import config from 'virtual:jaad/config'
|
|
6
|
+
import { collectionIdFor, collectionName } from './model/ids.ts'
|
|
7
|
+
|
|
8
|
+
const LOCALES = config.locales.map((locale) => locale.code)
|
|
9
|
+
|
|
10
|
+
/** The markdown of one directory, as the pages of `section` when there is one. */
|
|
11
|
+
const docs = (base: string, section?: string) =>
|
|
12
|
+
defineCollection({
|
|
13
|
+
loader: glob({
|
|
14
|
+
pattern: '**/*.md',
|
|
15
|
+
base,
|
|
16
|
+
// Keep URLs file-driven even if markdown contains an unrelated slug field.
|
|
17
|
+
generateId: ({ entry }) => collectionIdFor(entry, section, LOCALES),
|
|
18
|
+
}),
|
|
19
|
+
// Every field is optional: markdown without frontmatter works as-is.
|
|
20
|
+
schema: z.object({
|
|
21
|
+
...frontmatterFields(z),
|
|
22
|
+
/** Shorter label, in navigation only. */
|
|
23
|
+
label: z.string().optional(),
|
|
24
|
+
/** Pairs this page with its translations when the slugs differ. */
|
|
25
|
+
translationKey: z.string().optional(),
|
|
26
|
+
/** Replaces the filename prefix number. */
|
|
27
|
+
order: z.number().int().optional(),
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/** `docsPages` for `docsDir`, and one collection per section: each watches its
|
|
32
|
+
* own directory. */
|
|
33
|
+
export const collections = Object.fromEntries([
|
|
34
|
+
...(config.docs.dirInUse ? [[collectionName(), docs(config.docs.dir)]] : []),
|
|
35
|
+
...config.docs.sections.map((section) => [collectionName(section.key), docs(section.dir, section.key)]),
|
|
36
|
+
])
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { DocsPage } from './model/tree.ts'
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { getDocsTree } from './collection.ts'
|
|
4
|
+
import { formatChapterTitle } from './model/ids.ts'
|
|
5
|
+
import { pageDescription } from './model/rendered.ts'
|
|
6
|
+
import { SECTIONS, docsUrls } from './runtime.ts'
|
|
7
|
+
import { buildSearchIndex } from './search-index.ts'
|
|
8
|
+
import { docsMarkdownHref } from './urls.ts'
|
|
9
|
+
|
|
10
|
+
export async function searchIndexResponse(locale?: string): Promise<Response> {
|
|
11
|
+
const index = buildSearchIndex(await getDocsTree(locale), SECTIONS)
|
|
12
|
+
|
|
13
|
+
return new Response(JSON.stringify(index), {
|
|
14
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Row {
|
|
19
|
+
title: string
|
|
20
|
+
slug: string
|
|
21
|
+
description: string | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Plain-text index of every page, per the llms.txt convention. */
|
|
25
|
+
export async function llmsResponse(site: URL | undefined, locale?: string): Promise<Response> {
|
|
26
|
+
const { pages: sortedPages } = await getDocsTree(locale)
|
|
27
|
+
|
|
28
|
+
const markdownHref = (slug: string): string => {
|
|
29
|
+
const href = docsMarkdownHref(slug, docsUrls(locale))
|
|
30
|
+
return site ? new URL(href, site.origin).href : href
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const toLine = (row: Row): string =>
|
|
34
|
+
`- [${row.title}](${markdownHref(row.slug)})${row.description ? `: ${row.description}` : ''}`
|
|
35
|
+
|
|
36
|
+
/** Standalone pages first, then one heading per chapter. */
|
|
37
|
+
const outline = (pages: DocsPage[], heading: string): string[] => {
|
|
38
|
+
const standalone: Row[] = []
|
|
39
|
+
const chapters = new Map<string, Row[]>()
|
|
40
|
+
|
|
41
|
+
for (const page of pages) {
|
|
42
|
+
const { chapter, title } = page
|
|
43
|
+
const row: Row = {
|
|
44
|
+
title,
|
|
45
|
+
slug: page.slug,
|
|
46
|
+
description: page.data.description ?? pageDescription(page.source.rendered, title),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!chapter) {
|
|
50
|
+
standalone.push(row)
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rows = chapters.get(chapter) ?? []
|
|
55
|
+
if (rows.length === 0) chapters.set(chapter, rows)
|
|
56
|
+
rows.push(row)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return [
|
|
60
|
+
...(standalone.length > 0 ? [standalone.map(toLine).join('\n')] : []),
|
|
61
|
+
...[...chapters].map(
|
|
62
|
+
([chapter, rows]) =>
|
|
63
|
+
`${heading} ${formatChapterTitle(chapter) ?? chapter}\n\n${rows.map(toLine).join('\n')}`,
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// With sections, the pages outside them come first; each section is a
|
|
69
|
+
// heading, and its chapters sit one level below.
|
|
70
|
+
const loose = sortedPages.filter((page) => !page.section)
|
|
71
|
+
const sections: string[] =
|
|
72
|
+
SECTIONS.length === 0
|
|
73
|
+
? outline(sortedPages, '##')
|
|
74
|
+
: [
|
|
75
|
+
...(loose.length > 0 ? outline(loose, '##') : []),
|
|
76
|
+
...SECTIONS.flatMap((section) => {
|
|
77
|
+
const pages = sortedPages.filter((page) => page.section === section.key)
|
|
78
|
+
return pages.length > 0 ? [`## ${section.label}`, ...outline(pages, '###')] : []
|
|
79
|
+
}),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
const content = [`# ${config.title}`, ...(config.description ? [`> ${config.description}`] : []), ...sections].join(
|
|
83
|
+
'\n\n',
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return new Response(content + '\n', {
|
|
87
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
88
|
+
})
|
|
89
|
+
}
|