@jaamd/jaad 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +52 -2
- package/dist/jaad.css +1446 -0
- package/index.ts +45 -0
- package/package.json +70 -3
- package/site.ts +19 -0
- package/src/@types/docs.ts +47 -0
- package/src/advanced.ts +21 -0
- package/src/collection.ts +97 -0
- package/src/components/Footer.astro +18 -0
- package/src/components/Head.astro +119 -0
- package/src/components/Header.astro +47 -0
- package/src/components/LocaleSwitcher.astro +111 -0
- package/src/components/Nav.astro +22 -0
- package/src/components/NavMobile.astro +105 -0
- package/src/components/ScrollToTop.astro +43 -0
- package/src/components/SearchPalette.astro +328 -0
- package/src/components/SocialLinks.astro +27 -0
- package/src/components/ThemeSwitcher.astro +55 -0
- package/src/components/docs/Breadcrumbs.astro +36 -0
- package/src/components/docs/DocsMobileNav.astro +93 -0
- package/src/components/docs/DocsPage.astro +68 -0
- package/src/components/docs/DocsPageNav.astro +53 -0
- package/src/components/docs/DocsSidebarLeft.astro +82 -0
- package/src/components/docs/PageActions.astro +81 -0
- package/src/components/docs/StructuredData.astro +55 -0
- package/src/components/docs/TableOfContents.astro +73 -0
- package/src/components/locale-switcher.ts +6 -0
- package/src/config.ts +236 -0
- package/src/content.ts +42 -0
- package/src/docs-config.ts +30 -0
- package/src/env.d.ts +13 -0
- package/src/fonts/OFL.txt +94 -0
- package/src/fonts/inter.woff2 +0 -0
- package/src/fonts/merriweather-italic.woff2 +0 -0
- package/src/fonts/merriweather.woff2 +0 -0
- package/src/fonts.ts +29 -0
- package/src/forges.ts +60 -0
- package/src/infer.ts +75 -0
- package/src/integration.ts +270 -0
- package/src/layouts/DefaultDocsFrame.astro +31 -0
- package/src/layouts/Docs.astro +168 -0
- package/src/layouts/Page.astro +38 -0
- package/src/locale-tree.ts +51 -0
- package/src/locales.ts +191 -0
- package/src/navigation.ts +97 -0
- package/src/routes/404.astro +16 -0
- package/src/routes/docs-index.astro +19 -0
- package/src/routes/docs-slug.astro +58 -0
- package/src/routes/docs-slug.md.ts +37 -0
- package/src/routes/llms-locale.txt.ts +8 -0
- package/src/routes/llms.txt.ts +4 -0
- package/src/routes/search-index-locale.json.ts +8 -0
- package/src/routes/search-index.json.ts +4 -0
- package/src/social.ts +44 -0
- package/src/styles/components/breadcrumbs.css +13 -0
- package/src/styles/components/docs-mobile-nav.css +9 -0
- package/src/styles/components/docs-page-nav.css +31 -0
- package/src/styles/components/docs-page.css +9 -0
- package/src/styles/components/docs-sidebar.css +42 -0
- package/src/styles/components/footer.css +8 -0
- package/src/styles/components/header.css +20 -0
- package/src/styles/components/locale-switcher.css +20 -0
- package/src/styles/components/nav-mobile.css +15 -0
- package/src/styles/components/nav.css +11 -0
- package/src/styles/components/not-found.css +11 -0
- package/src/styles/components/page-actions.css +13 -0
- package/src/styles/components/scroll-to-top.css +11 -0
- package/src/styles/components/search.css +98 -0
- package/src/styles/components/shell.css +73 -0
- package/src/styles/components/social-links.css +8 -0
- package/src/styles/components/theme-switcher.css +14 -0
- package/src/styles/components/toc.css +21 -0
- package/src/styles/docs.css +112 -0
- package/src/styles/global.css +162 -0
- package/src/styles/jaamd-forward.css +16 -0
- package/src/styles/jaamd-reverse.css +18 -0
- package/src/styles/jaamd.css +11 -0
- package/src/styles/tailwind.css +30 -0
- package/src/themes/index.ts +26 -0
- package/src/ui/en.ts +29 -0
- package/src/ui/it.ts +30 -0
- package/src/ui.ts +49 -0
- package/src/urls.ts +49 -0
- package/src/utils/docs.ts +300 -0
- package/src/utils/endpoints.ts +80 -0
- package/src/utils/helpers.ts +4 -0
- package/src/utils/search.ts +83 -0
- package/src/virtual.ts +48 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsEntry, DocsHeadings } from '../@types/docs.ts'
|
|
3
|
+
import type { DocsFrameProps } from '../advanced.ts'
|
|
4
|
+
import type { DocsNavigation } from '../navigation.ts'
|
|
5
|
+
import { ClientRouter } from 'astro:transitions'
|
|
6
|
+
import config from 'virtual:jaad/config'
|
|
7
|
+
import DocsFrame from 'virtual:jaad/docs-frame'
|
|
8
|
+
import { getLocaleAlternates } from '../collection.ts'
|
|
9
|
+
import DocsSidebarLeft from '../components/docs/DocsSidebarLeft.astro'
|
|
10
|
+
import StructuredData from '../components/docs/StructuredData.astro'
|
|
11
|
+
import TableOfContents from '../components/docs/TableOfContents.astro'
|
|
12
|
+
import Footer from '../components/Footer.astro'
|
|
13
|
+
import Head from '../components/Head.astro'
|
|
14
|
+
import Header from '../components/Header.astro'
|
|
15
|
+
import { DEFAULT_LOCALE, LOCALES } from '../docs-config.ts'
|
|
16
|
+
import { docTitle, docTranslationKey, extractDescription, getCleanSlug, routedId } from '../utils/docs.ts'
|
|
17
|
+
import '../../dist/jaad.css'
|
|
18
|
+
import '../styles/docs.css'
|
|
19
|
+
import '../styles/jaamd.css'
|
|
20
|
+
import '@jaamd/jaamd/styles.css'
|
|
21
|
+
import 'virtual:jaad/theme.css'
|
|
22
|
+
import 'virtual:jaad/bridge.css'
|
|
23
|
+
import 'virtual:jaad/user.css'
|
|
24
|
+
import 'virtual:jaad/tailwind.css'
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
title?: string
|
|
28
|
+
chapter?: string
|
|
29
|
+
entry?: DocsEntry
|
|
30
|
+
headings?: DocsHeadings[]
|
|
31
|
+
navigation: DocsNavigation
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { title, chapter, entry, headings = [], navigation } = Astro.props
|
|
35
|
+
|
|
36
|
+
const pageTitle = title ? `${title} | ${config.title}` : config.title
|
|
37
|
+
|
|
38
|
+
// The opening page gets no `title`; resolve it from the entry.
|
|
39
|
+
const entryTitle = entry ? docTitle(entry) : (title ?? '')
|
|
40
|
+
|
|
41
|
+
const pageDescription =
|
|
42
|
+
entry?.data.description ??
|
|
43
|
+
(entry ? extractDescription(entry.body || '', entryTitle) : null) ??
|
|
44
|
+
config.description ??
|
|
45
|
+
config.title
|
|
46
|
+
|
|
47
|
+
const locale = entry?.locale ?? DEFAULT_LOCALE
|
|
48
|
+
const language = LOCALES.find((known) => known.code === locale)?.tag ?? config.lang
|
|
49
|
+
|
|
50
|
+
const switcherAlternates = await getLocaleAlternates(entry ? docTranslationKey(entry) : '')
|
|
51
|
+
|
|
52
|
+
// x-default is a landing place, so it takes the fallback when this page has no
|
|
53
|
+
// translation into the default locale.
|
|
54
|
+
const xDefault = switcherAlternates.find((alternate) => alternate.locale.code === DEFAULT_LOCALE)?.href
|
|
55
|
+
|
|
56
|
+
// hreflang describes translations of this page, not the switcher's fallbacks.
|
|
57
|
+
const alternates = switcherAlternates
|
|
58
|
+
.filter((alternate) => alternate.translated)
|
|
59
|
+
.map((alternate) => ({
|
|
60
|
+
code: alternate.locale.code,
|
|
61
|
+
tag: alternate.locale.tag,
|
|
62
|
+
href: alternate.href,
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
const frameProps: DocsFrameProps = {
|
|
66
|
+
page: {
|
|
67
|
+
id: entry?.id ?? '',
|
|
68
|
+
slug: entry ? getCleanSlug(routedId(entry)) : '',
|
|
69
|
+
title: entryTitle,
|
|
70
|
+
chapter,
|
|
71
|
+
data: entry?.data ?? { draft: false, keywords: [] },
|
|
72
|
+
},
|
|
73
|
+
headings,
|
|
74
|
+
navigation,
|
|
75
|
+
}
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<!doctype html>
|
|
79
|
+
<html lang={language} class={config.appearance === 'dark' ? 'dark' : undefined}>
|
|
80
|
+
<head>
|
|
81
|
+
<Head
|
|
82
|
+
title={pageTitle}
|
|
83
|
+
description={pageDescription}
|
|
84
|
+
keywords={entry?.data.keywords}
|
|
85
|
+
author={entry?.data.author}
|
|
86
|
+
image={entry?.data.ogImage}
|
|
87
|
+
modified={entry?.data.lastUpdated}
|
|
88
|
+
language={language}
|
|
89
|
+
{alternates}
|
|
90
|
+
{xDefault}
|
|
91
|
+
/>
|
|
92
|
+
<StructuredData
|
|
93
|
+
title={entryTitle || config.title}
|
|
94
|
+
description={pageDescription}
|
|
95
|
+
chapter={chapter}
|
|
96
|
+
keywords={entry?.data.keywords}
|
|
97
|
+
author={entry?.data.author}
|
|
98
|
+
modified={entry?.data.lastUpdated}
|
|
99
|
+
{language}
|
|
100
|
+
{locale}
|
|
101
|
+
/>
|
|
102
|
+
<ClientRouter />
|
|
103
|
+
</head>
|
|
104
|
+
<body data-docs class="jaad-body">
|
|
105
|
+
<DocsFrame {...frameProps}>
|
|
106
|
+
<Header slot="header" inDocs {locale} alternates={switcherAlternates} />
|
|
107
|
+
<DocsSidebarLeft slot="sidebar" sections={navigation.sections} {locale} />
|
|
108
|
+
<slot name="mobile-navigation" slot="mobile-navigation" />
|
|
109
|
+
<slot name="breadcrumbs" slot="breadcrumbs" />
|
|
110
|
+
<slot name="page-actions" slot="page-actions" />
|
|
111
|
+
<slot name="content" slot="content" />
|
|
112
|
+
<slot name="page-navigation" slot="page-navigation" />
|
|
113
|
+
<TableOfContents slot="table-of-contents" headings={headings} {locale} />
|
|
114
|
+
<Footer slot="footer" />
|
|
115
|
+
</DocsFrame>
|
|
116
|
+
|
|
117
|
+
<script>
|
|
118
|
+
import { navigate } from 'astro:transitions/client'
|
|
119
|
+
|
|
120
|
+
/** Identified by `rel`. */
|
|
121
|
+
function getPageLinks(): {
|
|
122
|
+
prev: HTMLAnchorElement | null
|
|
123
|
+
next: HTMLAnchorElement | null
|
|
124
|
+
} {
|
|
125
|
+
const nav = document.querySelector('nav[aria-label="Page navigation"]')
|
|
126
|
+
if (!nav) return { prev: null, next: null }
|
|
127
|
+
return {
|
|
128
|
+
prev: nav.querySelector<HTMLAnchorElement>('a[rel~="prev"][href]'),
|
|
129
|
+
next: nav.querySelector<HTMLAnchorElement>('a[rel~="next"][href]'),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A wide table or code block: the arrow keys should scroll it instead. */
|
|
134
|
+
function inHorizontallyScrollable(el: Element | null): boolean {
|
|
135
|
+
for (let node = el; node && node !== document.body;) {
|
|
136
|
+
if (node instanceof HTMLElement) {
|
|
137
|
+
const overflowX = getComputedStyle(node).overflowX
|
|
138
|
+
if ((overflowX === 'auto' || overflowX === 'scroll') && node.scrollWidth > node.clientWidth)
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
node = node.parentElement
|
|
142
|
+
}
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function onKey(e: KeyboardEvent) {
|
|
147
|
+
// Leave modified chords alone, notably the browser's back/forward.
|
|
148
|
+
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return
|
|
149
|
+
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
|
|
150
|
+
|
|
151
|
+
const active = document.activeElement as HTMLElement | null
|
|
152
|
+
const tag = active?.tagName
|
|
153
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
|
|
154
|
+
if (active?.isContentEditable) return
|
|
155
|
+
if (inHorizontallyScrollable(active)) return
|
|
156
|
+
|
|
157
|
+
const { prev, next } = getPageLinks()
|
|
158
|
+
const target = e.key === 'ArrowLeft' ? prev : next
|
|
159
|
+
if (!target) return
|
|
160
|
+
e.preventDefault()
|
|
161
|
+
navigate(target.href)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// `document` survives View Transitions, and this module runs once.
|
|
165
|
+
document.addEventListener('keydown', onKey)
|
|
166
|
+
</script>
|
|
167
|
+
</body>
|
|
168
|
+
</html>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import Footer from '../components/Footer.astro'
|
|
4
|
+
import Head from '../components/Head.astro'
|
|
5
|
+
import Header from '../components/Header.astro'
|
|
6
|
+
import '../../dist/jaad.css'
|
|
7
|
+
import 'virtual:jaad/theme.css'
|
|
8
|
+
import 'virtual:jaad/bridge.css'
|
|
9
|
+
import 'virtual:jaad/user.css'
|
|
10
|
+
import 'virtual:jaad/tailwind.css'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
title?: string
|
|
14
|
+
description?: string
|
|
15
|
+
bare?: boolean
|
|
16
|
+
/** Added to the body. */
|
|
17
|
+
class?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { title, description, bare = false, class: className } = Astro.props
|
|
21
|
+
|
|
22
|
+
const pageTitle = title ? `${title} | ${config.title}` : config.title
|
|
23
|
+
const pageDescription = description ?? config.description ?? config.title
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<!doctype html>
|
|
27
|
+
<html lang={config.lang} class={config.appearance === 'dark' ? 'dark' : undefined}>
|
|
28
|
+
<head>
|
|
29
|
+
<Head title={pageTitle} description={pageDescription} />
|
|
30
|
+
</head>
|
|
31
|
+
<body class:list={['jaad-body', className]}>
|
|
32
|
+
<Header />
|
|
33
|
+
<main class={bare ? 'jaad-main-bare' : 'jaad-main'}>
|
|
34
|
+
<slot />
|
|
35
|
+
</main>
|
|
36
|
+
<Footer />
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { isLocale } from './locales.ts'
|
|
4
|
+
|
|
5
|
+
function hasMarkdown(dir: string): boolean {
|
|
6
|
+
return readdirSync(dir, { withFileTypes: true }).some((entry) =>
|
|
7
|
+
entry.isDirectory() ? hasMarkdown(join(dir, entry.name)) : entry.name.endsWith('.md'),
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Locale directories under docs/, or none. Two mean the tree is organised by
|
|
12
|
+
* language; one counts only when `lang` names it. */
|
|
13
|
+
export function detectLocales(docsDir: string, lang: string): string[] {
|
|
14
|
+
if (!existsSync(docsDir)) return []
|
|
15
|
+
|
|
16
|
+
const candidates = readdirSync(docsDir, { withFileTypes: true })
|
|
17
|
+
.filter((entry) => entry.isDirectory() && isLocale(entry.name))
|
|
18
|
+
.map((entry) => entry.name.toLowerCase())
|
|
19
|
+
.toSorted()
|
|
20
|
+
|
|
21
|
+
const localised = candidates.length >= 2 || (candidates.length === 1 && candidates[0] === lang.toLowerCase())
|
|
22
|
+
if (!localised) return []
|
|
23
|
+
|
|
24
|
+
// An empty directory would reach the switcher and answer 404.
|
|
25
|
+
return candidates.filter((code) => hasMarkdown(join(docsDir, code)))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Once localised, every top-level entry is a locale and `lang` names one. */
|
|
29
|
+
export function validateLocaleTree(docsDir: string, locales: string[], lang: string): void {
|
|
30
|
+
const issues: string[] = []
|
|
31
|
+
|
|
32
|
+
const strays = readdirSync(docsDir, { withFileTypes: true })
|
|
33
|
+
.filter((entry) => !(entry.isDirectory() && isLocale(entry.name)))
|
|
34
|
+
.map((entry) => entry.name)
|
|
35
|
+
.toSorted()
|
|
36
|
+
|
|
37
|
+
for (const stray of strays) {
|
|
38
|
+
issues.push(`docs/${stray} is not a locale, but ${locales.join(', ')} are`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!locales.includes(lang.toLowerCase())) {
|
|
42
|
+
issues.push(`lang is "${lang}", which has no directory; found ${locales.join(', ')}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (issues.length > 0) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`jaad: mixed documentation tree\n${issues.map((issue) => ` ${issue}`).join('\n')}\n` +
|
|
48
|
+
'Move every page under a locale directory, or set locales: false.',
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/locales.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { ucfirst } from './utils/helpers.ts'
|
|
2
|
+
|
|
3
|
+
/** ISO 639-1. A three-letter directory such as `api` is never a locale. */
|
|
4
|
+
const LANGUAGES = new Set(
|
|
5
|
+
(
|
|
6
|
+
'aa ab ae af ak am an ar as av ay az ba be bg bi bm bn bo br bs ca ce ch co ' +
|
|
7
|
+
'cr cs cu cv cy da de dv dz ee el en eo es et eu fa ff fi fj fo fr fy ga gd ' +
|
|
8
|
+
'gl gn gu gv ha he hi ho hr ht hu hy hz ia id ie ig ii ik io is it iu ja jv ' +
|
|
9
|
+
'ka kg ki kj kk kl km kn ko kr ks ku kv kw ky la lb lg li ln lo lt lu lv mg ' +
|
|
10
|
+
'mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc oj om or os ' +
|
|
11
|
+
'pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg si sk sl sm sn so sq sr ss ' +
|
|
12
|
+
'st su sv sw ta te tg th ti tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo ' +
|
|
13
|
+
'wa wo xh yi yo za zh zu'
|
|
14
|
+
).split(' '),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
/** Region for a language that does not name one. Listed, never derived. */
|
|
18
|
+
const LANGUAGE_REGION: Record<string, string> = {
|
|
19
|
+
af: 'ZA',
|
|
20
|
+
am: 'ET',
|
|
21
|
+
ar: 'SA',
|
|
22
|
+
az: 'AZ',
|
|
23
|
+
be: 'BY',
|
|
24
|
+
bg: 'BG',
|
|
25
|
+
bn: 'BD',
|
|
26
|
+
bs: 'BA',
|
|
27
|
+
ca: 'ES',
|
|
28
|
+
cs: 'CZ',
|
|
29
|
+
cy: 'GB',
|
|
30
|
+
da: 'DK',
|
|
31
|
+
de: 'DE',
|
|
32
|
+
el: 'GR',
|
|
33
|
+
en: 'GB',
|
|
34
|
+
es: 'ES',
|
|
35
|
+
et: 'EE',
|
|
36
|
+
eu: 'ES',
|
|
37
|
+
fa: 'IR',
|
|
38
|
+
fi: 'FI',
|
|
39
|
+
fr: 'FR',
|
|
40
|
+
ga: 'IE',
|
|
41
|
+
gd: 'GB',
|
|
42
|
+
gl: 'ES',
|
|
43
|
+
gu: 'IN',
|
|
44
|
+
ha: 'NG',
|
|
45
|
+
he: 'IL',
|
|
46
|
+
hi: 'IN',
|
|
47
|
+
hr: 'HR',
|
|
48
|
+
hu: 'HU',
|
|
49
|
+
hy: 'AM',
|
|
50
|
+
id: 'ID',
|
|
51
|
+
ig: 'NG',
|
|
52
|
+
is: 'IS',
|
|
53
|
+
it: 'IT',
|
|
54
|
+
ja: 'JP',
|
|
55
|
+
jv: 'ID',
|
|
56
|
+
ka: 'GE',
|
|
57
|
+
kk: 'KZ',
|
|
58
|
+
km: 'KH',
|
|
59
|
+
kn: 'IN',
|
|
60
|
+
ko: 'KR',
|
|
61
|
+
ky: 'KG',
|
|
62
|
+
lo: 'LA',
|
|
63
|
+
lt: 'LT',
|
|
64
|
+
lv: 'LV',
|
|
65
|
+
mk: 'MK',
|
|
66
|
+
ml: 'IN',
|
|
67
|
+
mn: 'MN',
|
|
68
|
+
mr: 'IN',
|
|
69
|
+
ms: 'MY',
|
|
70
|
+
mt: 'MT',
|
|
71
|
+
my: 'MM',
|
|
72
|
+
nb: 'NO',
|
|
73
|
+
ne: 'NP',
|
|
74
|
+
nl: 'NL',
|
|
75
|
+
nn: 'NO',
|
|
76
|
+
no: 'NO',
|
|
77
|
+
pa: 'IN',
|
|
78
|
+
pl: 'PL',
|
|
79
|
+
ps: 'AF',
|
|
80
|
+
pt: 'PT',
|
|
81
|
+
ro: 'RO',
|
|
82
|
+
ru: 'RU',
|
|
83
|
+
si: 'LK',
|
|
84
|
+
sk: 'SK',
|
|
85
|
+
sl: 'SI',
|
|
86
|
+
so: 'SO',
|
|
87
|
+
sq: 'AL',
|
|
88
|
+
sr: 'RS',
|
|
89
|
+
su: 'ID',
|
|
90
|
+
sv: 'SE',
|
|
91
|
+
sw: 'TZ',
|
|
92
|
+
ta: 'IN',
|
|
93
|
+
te: 'IN',
|
|
94
|
+
tg: 'TJ',
|
|
95
|
+
th: 'TH',
|
|
96
|
+
tk: 'TM',
|
|
97
|
+
tl: 'PH',
|
|
98
|
+
tr: 'TR',
|
|
99
|
+
uk: 'UA',
|
|
100
|
+
ur: 'PK',
|
|
101
|
+
uz: 'UZ',
|
|
102
|
+
vi: 'VN',
|
|
103
|
+
yo: 'NG',
|
|
104
|
+
zh: 'CN',
|
|
105
|
+
zu: 'ZA',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** language[-Script][-REGION], lowercased: ids arrive slugged from the loader. */
|
|
109
|
+
const LOCALE_RE = /^([a-z]{2})(?:-([a-z]{4}))?(?:-([a-z]{2}))?$/
|
|
110
|
+
|
|
111
|
+
export interface Locale {
|
|
112
|
+
/** Lowercased directory name, and the URL segment. */
|
|
113
|
+
code: string
|
|
114
|
+
/** BCP-47, for `lang` and `hreflang`. */
|
|
115
|
+
tag: string
|
|
116
|
+
/** Native name, for the switcher menu. */
|
|
117
|
+
name: string
|
|
118
|
+
/** Emoji, or null for a language with no listed region. */
|
|
119
|
+
flag: string | null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isLocale(segment: string): boolean {
|
|
123
|
+
const match = LOCALE_RE.exec(segment.toLowerCase())
|
|
124
|
+
return match !== null && LANGUAGES.has(match[1])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** "pt-br" becomes "pt-BR", "zh-hans" becomes "zh-Hans". */
|
|
128
|
+
export function localeTag(code: string): string {
|
|
129
|
+
const match = LOCALE_RE.exec(code.toLowerCase())
|
|
130
|
+
if (!match) return code
|
|
131
|
+
const [, language, script, region] = match
|
|
132
|
+
return [language, script && ucfirst(script), region && region.toUpperCase()].filter(Boolean).join('-')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function localeName(tag: string): string {
|
|
136
|
+
try {
|
|
137
|
+
const names = new Intl.DisplayNames([tag], { type: 'language' })
|
|
138
|
+
return ucfirst(names.of(tag) ?? tag)
|
|
139
|
+
} catch {
|
|
140
|
+
return tag
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Regional indicators, A at U+1F1E6. */
|
|
145
|
+
function localeFlag(code: string): string | null {
|
|
146
|
+
const match = LOCALE_RE.exec(code.toLowerCase())
|
|
147
|
+
if (!match) return null
|
|
148
|
+
// A group that did not participate is undefined, whatever the type says.
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
150
|
+
const region = match[3]?.toUpperCase() ?? LANGUAGE_REGION[match[1]]
|
|
151
|
+
if (!region) return null
|
|
152
|
+
return String.fromCodePoint(...Array.from(region, (letter) => 0x1f1e6 + letter.charCodeAt(0) - 65))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Open Graph wants language_TERRITORY, not a BCP-47 tag. The territory comes
|
|
156
|
+
* from the code when it names one, and from LANGUAGE_REGION otherwise. */
|
|
157
|
+
export function openGraphLocale(code: string): string {
|
|
158
|
+
const match = LOCALE_RE.exec(code.toLowerCase())
|
|
159
|
+
if (!match) return code
|
|
160
|
+
|
|
161
|
+
const language = match[1]
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
163
|
+
const region = match[3]?.toUpperCase() ?? LANGUAGE_REGION[language]
|
|
164
|
+
return region ? `${language}_${region}` : language
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function describeLocale(code: string): Locale {
|
|
168
|
+
const tag = localeTag(code)
|
|
169
|
+
return { code, tag, name: localeName(tag), flag: localeFlag(code) }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Empty for the locale that owns the unprefixed urls. */
|
|
173
|
+
export function localeSegment(locale?: string, defaultLocale?: string): string {
|
|
174
|
+
return !locale || locale === defaultLocale ? '' : locale
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The segment a generated slug carries, with its separator. */
|
|
178
|
+
export function localePrefix(locale?: string, defaultLocale?: string): string {
|
|
179
|
+
const segment = localeSegment(locale, defaultLocale)
|
|
180
|
+
return segment ? `${segment}/` : ''
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** What to iterate when building: every locale, or one unlocalised pass. */
|
|
184
|
+
export function routedLocales(locales: Locale[]): (string | undefined)[] {
|
|
185
|
+
return locales.length > 0 ? locales.map((l) => l.code) : [undefined]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The locale codes whose urls carry a prefix. */
|
|
189
|
+
export function prefixedLocales(locales: Locale[], defaultLocale: string): string[] {
|
|
190
|
+
return locales.map((locale) => locale.code).filter((code) => localeSegment(code, defaultLocale) !== '')
|
|
191
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { DocsNavItem } from './@types/docs.ts'
|
|
2
|
+
import { docsPageHref, type DocsUrlOptions } from './urls.ts'
|
|
3
|
+
import { docLabel, getCanonicalDocSlug, parseDocCollectionId, routedId, type DocsPageLike } from './utils/docs.ts'
|
|
4
|
+
|
|
5
|
+
export type NavSection =
|
|
6
|
+
| { type: 'page'; order: number; item: DocsNavItem }
|
|
7
|
+
| { type: 'chapter'; order: number; chapter: string; items: DocsNavItem[] }
|
|
8
|
+
|
|
9
|
+
export interface DocsNavigation {
|
|
10
|
+
sections: NavSection[]
|
|
11
|
+
previousPage: DocsNavItem | null
|
|
12
|
+
nextPage: DocsNavItem | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function groupByChapter(items: DocsNavItem[]): NavSection[] {
|
|
16
|
+
const chapters = new Map<string, Extract<NavSection, { type: 'chapter' }>>()
|
|
17
|
+
const sections: NavSection[] = []
|
|
18
|
+
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
if (!item.chapter) {
|
|
21
|
+
sections.push({ type: 'page', order: item.primaryOrder, item })
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let chapter = chapters.get(item.chapter)
|
|
26
|
+
if (!chapter) {
|
|
27
|
+
chapter = {
|
|
28
|
+
type: 'chapter',
|
|
29
|
+
order: item.primaryOrder,
|
|
30
|
+
chapter: item.chapter,
|
|
31
|
+
items: [],
|
|
32
|
+
}
|
|
33
|
+
chapters.set(item.chapter, chapter)
|
|
34
|
+
sections.push(chapter)
|
|
35
|
+
}
|
|
36
|
+
chapter.items.push(item)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return sections.toSorted((a, b) => a.order - b.order)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build every navigation view from one ordered collection traversal. */
|
|
43
|
+
interface Model {
|
|
44
|
+
items: DocsNavItem[]
|
|
45
|
+
positions: Map<string, number>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Only isActive and the neighbours vary between the pages of one locale. The
|
|
49
|
+
// hrefs do not, as long as the url options are the same ones.
|
|
50
|
+
const models = new WeakMap<object, Map<string, Model>>()
|
|
51
|
+
|
|
52
|
+
function modelFor(sortedPages: DocsPageLike[], urls: DocsUrlOptions): Model {
|
|
53
|
+
const key = `${urls.docsBase}\u0000${urls.deploymentBase ?? ''}\u0000${urls.locale ?? ''}\u0000${urls.defaultLocale ?? ''}`
|
|
54
|
+
let byUrls = models.get(sortedPages)
|
|
55
|
+
if (!byUrls) {
|
|
56
|
+
byUrls = new Map()
|
|
57
|
+
models.set(sortedPages, byUrls)
|
|
58
|
+
}
|
|
59
|
+
const cached = byUrls.get(key)
|
|
60
|
+
if (cached) return cached
|
|
61
|
+
|
|
62
|
+
const model: Model = {
|
|
63
|
+
items: sortedPages.map((page, index): DocsNavItem => {
|
|
64
|
+
const parsed = parseDocCollectionId(routedId(page))
|
|
65
|
+
return {
|
|
66
|
+
title: docLabel(page),
|
|
67
|
+
chapter: parsed.chapter,
|
|
68
|
+
primaryOrder: parsed.orderChapter ?? page.data?.order ?? parsed.order,
|
|
69
|
+
href: docsPageHref(getCanonicalDocSlug(page, index), urls),
|
|
70
|
+
isActive: false,
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
positions: new Map(sortedPages.map((page, index) => [page.id, index])),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
byUrls.set(key, model)
|
|
77
|
+
return model
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildDocsNavigation(
|
|
81
|
+
sortedPages: DocsPageLike[],
|
|
82
|
+
currentId: string,
|
|
83
|
+
urls: DocsUrlOptions,
|
|
84
|
+
): DocsNavigation {
|
|
85
|
+
const { items, positions } = modelFor(sortedPages, urls)
|
|
86
|
+
const currentIndex = positions.get(currentId) ?? -1
|
|
87
|
+
const marked = items.map((item, index) => ({
|
|
88
|
+
...item,
|
|
89
|
+
isActive: index === currentIndex,
|
|
90
|
+
}))
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
sections: groupByChapter(marked),
|
|
94
|
+
previousPage: currentIndex > 0 ? marked[currentIndex - 1] : null,
|
|
95
|
+
nextPage: currentIndex >= 0 && currentIndex < marked.length - 1 ? marked[currentIndex + 1] : null,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from '../layouts/Page.astro'
|
|
3
|
+
import { withDeploymentBase } from '../urls.ts'
|
|
4
|
+
|
|
5
|
+
const homeHref = withDeploymentBase('/', import.meta.env.BASE_URL)
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<Layout title="404" description="The page you're looking for doesn't exist.">
|
|
9
|
+
<div class="jaad-not-found">
|
|
10
|
+
<h1 class="jaad-not-found-title">404</h1>
|
|
11
|
+
<p class="jaad-not-found-text">
|
|
12
|
+
<i>Oops!</i> The page you're looking for doesn't exist.
|
|
13
|
+
</p>
|
|
14
|
+
<a href={homeHref}>Go back to the homepage</a>
|
|
15
|
+
</div>
|
|
16
|
+
</Layout>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content'
|
|
3
|
+
import { getSortedDocsPages } from '../collection.ts'
|
|
4
|
+
import DocsPage from '../components/docs/DocsPage.astro'
|
|
5
|
+
import { withDeploymentBase } from '../urls.ts'
|
|
6
|
+
|
|
7
|
+
const sortedPages = await getSortedDocsPages()
|
|
8
|
+
|
|
9
|
+
const firstPage = sortedPages[0]
|
|
10
|
+
if (!firstPage) {
|
|
11
|
+
return Astro.redirect(withDeploymentBase('/404', import.meta.env.BASE_URL))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { Content } = await render(firstPage)
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<DocsPage entry={firstPage} {sortedPages}>
|
|
18
|
+
<Content />
|
|
19
|
+
</DocsPage>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content'
|
|
3
|
+
import { getSortedDocsPages } from '../collection.ts'
|
|
4
|
+
import DocsPage from '../components/docs/DocsPage.astro'
|
|
5
|
+
import { buildLocales, docsUrls, localePrefix } from '../docs-config.ts'
|
|
6
|
+
import { docsPageHref, withDeploymentBase } from '../urls.ts'
|
|
7
|
+
import { getCanonicalDocSlug, getCleanSlug, docTitle, routedId } from '../utils/docs.ts'
|
|
8
|
+
|
|
9
|
+
interface DocsPath {
|
|
10
|
+
params: { slug: string }
|
|
11
|
+
props: { id: string; locale?: string; isOpening: boolean }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function getStaticPaths() {
|
|
15
|
+
const paths: DocsPath[] = []
|
|
16
|
+
|
|
17
|
+
for (const locale of buildLocales()) {
|
|
18
|
+
const sortedPages = await getSortedDocsPages(locale)
|
|
19
|
+
const prefix = localePrefix(locale)
|
|
20
|
+
|
|
21
|
+
sortedPages.forEach((page, index) => {
|
|
22
|
+
const isOpening = getCanonicalDocSlug(page, index) === ''
|
|
23
|
+
paths.push({
|
|
24
|
+
params: { slug: `${prefix}${getCleanSlug(routedId(page))}` },
|
|
25
|
+
props: { id: page.id, locale, isOpening },
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// A prefixed locale has no index route of its own: /it is this page.
|
|
29
|
+
if (isOpening && prefix) {
|
|
30
|
+
paths.push({
|
|
31
|
+
params: { slug: locale! },
|
|
32
|
+
props: { id: page.id, locale, isOpening: false },
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return paths
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { id, locale, isOpening } = Astro.props
|
|
42
|
+
|
|
43
|
+
if (isOpening) {
|
|
44
|
+
return Astro.redirect(docsPageHref('', docsUrls(locale)), 301)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sortedPages = await getSortedDocsPages(locale)
|
|
48
|
+
const docPage = sortedPages.find((page) => page.id === id)
|
|
49
|
+
if (!docPage) {
|
|
50
|
+
return Astro.redirect(withDeploymentBase('/404', import.meta.env.BASE_URL))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { Content } = await render(docPage)
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<DocsPage entry={docPage} {sortedPages} headTitle={docTitle(docPage)}>
|
|
57
|
+
<Content />
|
|
58
|
+
</DocsPage>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { APIRoute, GetStaticPaths } from 'astro'
|
|
2
|
+
import { getSortedDocsPages } from '../collection.ts'
|
|
3
|
+
import { buildLocales, localePrefix } from '../docs-config.ts'
|
|
4
|
+
import { getCleanSlug, routedId } from '../utils/docs.ts'
|
|
5
|
+
|
|
6
|
+
/** Raw markdown at the page's own url plus `.md`, for readers and for LLMs. */
|
|
7
|
+
export const getStaticPaths: GetStaticPaths = async () => {
|
|
8
|
+
const paths: {
|
|
9
|
+
params: { slug: string }
|
|
10
|
+
props: { id: string; locale?: string }
|
|
11
|
+
}[] = []
|
|
12
|
+
|
|
13
|
+
for (const locale of buildLocales()) {
|
|
14
|
+
const sortedPages = await getSortedDocsPages(locale)
|
|
15
|
+
const prefix = localePrefix(locale)
|
|
16
|
+
|
|
17
|
+
for (const page of sortedPages) {
|
|
18
|
+
paths.push({
|
|
19
|
+
params: { slug: `${prefix}${getCleanSlug(routedId(page))}` },
|
|
20
|
+
props: { id: page.id, locale },
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return paths
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const GET: APIRoute = async ({ props }) => {
|
|
29
|
+
const { id, locale } = props as { id: string; locale?: string }
|
|
30
|
+
const pages = await getSortedDocsPages(locale)
|
|
31
|
+
const entry = pages.find((page) => page.id === id)
|
|
32
|
+
if (!entry) return new Response('Not found', { status: 404 })
|
|
33
|
+
|
|
34
|
+
return new Response(entry.body ?? '', {
|
|
35
|
+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { APIRoute, GetStaticPaths } from 'astro'
|
|
2
|
+
import { getServedLocales } from '../collection.ts'
|
|
3
|
+
import { llmsResponse } from '../utils/endpoints.ts'
|
|
4
|
+
|
|
5
|
+
export const getStaticPaths: GetStaticPaths = async () =>
|
|
6
|
+
(await getServedLocales()).map((locale) => ({ params: { locale } }))
|
|
7
|
+
|
|
8
|
+
export const GET: APIRoute = ({ site, params }) => llmsResponse(site, params.locale)
|