@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,249 @@
|
|
|
1
|
+
import type { JaadResolvedConfig } from '../config.ts'
|
|
2
|
+
import type { DocsRoot } from './config.ts'
|
|
3
|
+
import type { AstroIntegration } from 'astro'
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
5
|
+
import { join, relative } from 'node:path'
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import { prefixedLocales } from '../site/locales.ts'
|
|
8
|
+
import { normaliseBasePath } from '../site/urls.ts'
|
|
9
|
+
import { docsRoots } from './config.ts'
|
|
10
|
+
import { rehypeDocLinks, satteriDocLinks } from './link-plugins.ts'
|
|
11
|
+
import { getDocCollectionId, splitDocLocale } from './model/ids.ts'
|
|
12
|
+
import { buildDocsTree, type DocsSource } from './model/tree.ts'
|
|
13
|
+
import { docsPageHref, type DocsUrlOptions } from './urls.ts'
|
|
14
|
+
|
|
15
|
+
type SetupParams = Parameters<NonNullable<AstroIntegration['hooks']['astro:config:setup']>>[0]
|
|
16
|
+
|
|
17
|
+
const DOCS_FRAME_ID = 'virtual:jaad/docs-frame'
|
|
18
|
+
const RESOLVED_DOCS_FRAME = '\0' + DOCS_FRAME_ID
|
|
19
|
+
|
|
20
|
+
/** The frame around every docs page: the project's own, or JAAD's. */
|
|
21
|
+
function docsFrameVirtualPlugin(docsFrame: () => string) {
|
|
22
|
+
return {
|
|
23
|
+
name: 'jaad:docs-frame',
|
|
24
|
+
resolveId: (source: string) => (source === DOCS_FRAME_ID ? RESOLVED_DOCS_FRAME : null),
|
|
25
|
+
load: (id: string) =>
|
|
26
|
+
id === RESOLVED_DOCS_FRAME ? `export { default } from ${JSON.stringify(docsFrame())};` : null,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A named url that redirects to its page's canonical one. */
|
|
31
|
+
export interface DocsAlias {
|
|
32
|
+
locale?: string
|
|
33
|
+
slug: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IntegrationState {
|
|
37
|
+
deploymentBase: string
|
|
38
|
+
aliases: DocsAlias[]
|
|
39
|
+
docsFrame: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const INDEX_PAGE_NAMES = ['index.astro', 'index.md', 'index.mdx', 'index.html', 'index.js', 'index.ts']
|
|
43
|
+
|
|
44
|
+
const NOT_FOUND_PAGE_NAMES = ['404.astro', '404.md', '404.mdx', '404.html', '404.js', '404.ts']
|
|
45
|
+
|
|
46
|
+
/** A plugin this integration registered on an earlier setup run. */
|
|
47
|
+
const isDocLinks = (plugin: unknown): boolean =>
|
|
48
|
+
(typeof plugin === 'function' || (typeof plugin === 'object' && plugin !== null)) &&
|
|
49
|
+
['jaad-doc-links', 'jaadDocLinks'].includes((plugin as { name?: string }).name ?? '')
|
|
50
|
+
|
|
51
|
+
const routeEntrypoint = (file: string) => fileURLToPath(new URL(`./routes/${file}`, import.meta.url))
|
|
52
|
+
|
|
53
|
+
export function shouldInjectNotFound(docsBase: string, srcDir: URL): boolean {
|
|
54
|
+
return docsBase === '' && !NOT_FOUND_PAGE_NAMES.some((name) => existsSync(new URL(`pages/${name}`, srcDir)))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The two fields that move the first page of the docs or of a section. */
|
|
58
|
+
function readFrontmatter(source: string): { order?: number; draft?: boolean } {
|
|
59
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(source)
|
|
60
|
+
if (!block) return {}
|
|
61
|
+
|
|
62
|
+
const order = /^order:\s*(-?\d+)\s*$/m.exec(block[1])
|
|
63
|
+
const draft = /^draft:\s*(true|false)\s*$/m.exec(block[1])
|
|
64
|
+
return {
|
|
65
|
+
order: order ? Number(order[1]) : undefined,
|
|
66
|
+
draft: draft ? draft[1] === 'true' : undefined,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The aliases of every locale. The content collection is not available this
|
|
71
|
+
* early, so the pages are read from disk. */
|
|
72
|
+
export function findAliases(roots: DocsRoot[], locales: string[] = []): DocsAlias[] {
|
|
73
|
+
const sources: DocsSource[] = []
|
|
74
|
+
|
|
75
|
+
const visit = (root: DocsRoot, current: string) => {
|
|
76
|
+
for (const entry of readdirSync(current, { withFileTypes: true }).toSorted((a, b) =>
|
|
77
|
+
a.name.localeCompare(b.name),
|
|
78
|
+
)) {
|
|
79
|
+
const file = join(current, entry.name)
|
|
80
|
+
if (entry.isDirectory()) visit(root, file)
|
|
81
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
82
|
+
const front = readFrontmatter(readFileSync(file, 'utf8'))
|
|
83
|
+
if (front.draft) continue
|
|
84
|
+
|
|
85
|
+
const { locale, id } = splitDocLocale(getDocCollectionId(relative(root.dir, file)), locales)
|
|
86
|
+
sources.push({
|
|
87
|
+
id,
|
|
88
|
+
path: id,
|
|
89
|
+
locale: locale ?? undefined,
|
|
90
|
+
section: root.section,
|
|
91
|
+
data: { order: front.order },
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const root of roots) {
|
|
97
|
+
if (existsSync(root.dir)) visit(root, root.dir)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const sections = roots.flatMap((root) => root.section ?? [])
|
|
101
|
+
return (locales.length > 0 ? locales : [undefined]).flatMap((locale) => {
|
|
102
|
+
// The slugs are all that is read: the urls need no mount.
|
|
103
|
+
const tree = buildDocsTree(
|
|
104
|
+
sources.filter((source) => source.locale === locale),
|
|
105
|
+
{ sections, urls: { docsBase: '' } },
|
|
106
|
+
)
|
|
107
|
+
return tree.pages.flatMap((page) => page.aliases.map((slug) => ({ locale, slug })))
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getInjectedRoutes(docsBase: string, locales: string[] = []): [string, string][] {
|
|
112
|
+
// `[...slug]` already absorbs "it/guide" and "it". Only the two fixed
|
|
113
|
+
// endpoints need a route of their own, and `[locale]` hands them the code.
|
|
114
|
+
const perLocale: [string, string][] =
|
|
115
|
+
locales.length > 0
|
|
116
|
+
? [
|
|
117
|
+
[`${docsBase}/[locale]/search-index.json`, 'search-index-locale.json.ts'],
|
|
118
|
+
[`${docsBase}/[locale]/llms.txt`, 'llms-locale.txt.ts'],
|
|
119
|
+
]
|
|
120
|
+
: []
|
|
121
|
+
|
|
122
|
+
return [
|
|
123
|
+
[docsBase, 'docs-index.astro'],
|
|
124
|
+
[`${docsBase}/search-index.json`, 'search-index.json.ts'],
|
|
125
|
+
[`${docsBase}/llms.txt`, 'llms.txt.ts'],
|
|
126
|
+
...perLocale,
|
|
127
|
+
[`${docsBase}/[...slug]`, 'docs-slug.astro'],
|
|
128
|
+
[`${docsBase}/[...slug].md`, 'docs-slug.md.ts'],
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createIntegrationState(): IntegrationState {
|
|
133
|
+
return {
|
|
134
|
+
deploymentBase: '',
|
|
135
|
+
aliases: [],
|
|
136
|
+
docsFrame: fileURLToPath(new URL('./layouts/DefaultDocsFrame.astro', import.meta.url)),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Routes, the docs frame, the 404 page, aliases and the link plugins. */
|
|
141
|
+
export function setupDocs(
|
|
142
|
+
{ config: astroConfig, updateConfig, injectRoute, addWatchFile, logger }: SetupParams,
|
|
143
|
+
config: JaadResolvedConfig,
|
|
144
|
+
state: IntegrationState,
|
|
145
|
+
discoverAliases: boolean,
|
|
146
|
+
): void {
|
|
147
|
+
updateConfig({ vite: { plugins: [docsFrameVirtualPlugin(() => state.docsFrame)] } })
|
|
148
|
+
|
|
149
|
+
for (const [pattern, entrypoint] of getInjectedRoutes(
|
|
150
|
+
config.docs.base,
|
|
151
|
+
prefixedLocales(config.locales, config.defaultLocale),
|
|
152
|
+
)) {
|
|
153
|
+
injectRoute({
|
|
154
|
+
pattern,
|
|
155
|
+
entrypoint: routeEntrypoint(entrypoint),
|
|
156
|
+
prerender: true,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const customDocsFrame = new URL('jaad/DocsFrame.astro', astroConfig.srcDir)
|
|
161
|
+
if (existsSync(customDocsFrame)) {
|
|
162
|
+
state.docsFrame = fileURLToPath(customDocsFrame)
|
|
163
|
+
addWatchFile(customDocsFrame)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (shouldInjectNotFound(config.docs.base, astroConfig.srcDir)) {
|
|
167
|
+
injectRoute({
|
|
168
|
+
pattern: '/404',
|
|
169
|
+
entrypoint: routeEntrypoint('404.astro'),
|
|
170
|
+
prerender: true,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const root = fileURLToPath(astroConfig.root)
|
|
175
|
+
const roots = docsRoots(config.docs, root)
|
|
176
|
+
const locales = config.locales.map((locale) => locale.code)
|
|
177
|
+
|
|
178
|
+
const missing = roots.filter(({ dir }) => !existsSync(dir))
|
|
179
|
+
for (const { dir } of missing) {
|
|
180
|
+
logger.warn(
|
|
181
|
+
`${relative(root, dir)} does not exist, so it adds no pages. ` +
|
|
182
|
+
'Create it, or point docsDir or the section at your markdown.',
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (discoverAliases && missing.length < roots.length) {
|
|
186
|
+
state.aliases = findAliases(roots, locales)
|
|
187
|
+
}
|
|
188
|
+
for (const { dir } of roots) addWatchFile(pathToFileURL(dir))
|
|
189
|
+
|
|
190
|
+
const links = {
|
|
191
|
+
roots,
|
|
192
|
+
locales,
|
|
193
|
+
urls: (locale?: string): DocsUrlOptions => ({
|
|
194
|
+
docsBase: config.docs.base,
|
|
195
|
+
deploymentBase: normaliseBasePath(astroConfig.base),
|
|
196
|
+
locale,
|
|
197
|
+
defaultLocale: config.defaultLocale,
|
|
198
|
+
}),
|
|
199
|
+
}
|
|
200
|
+
// New arrays: a shared processor would stack duplicates across setup runs.
|
|
201
|
+
const processor = astroConfig.markdown.processor as {
|
|
202
|
+
name: string
|
|
203
|
+
options: { hastPlugins?: unknown[]; rehypePlugins?: unknown[] }
|
|
204
|
+
}
|
|
205
|
+
if (processor.name === 'satteri') {
|
|
206
|
+
processor.options.hastPlugins = [
|
|
207
|
+
...(processor.options.hastPlugins ?? []).filter((plugin) => !isDocLinks(plugin)),
|
|
208
|
+
satteriDocLinks(links),
|
|
209
|
+
]
|
|
210
|
+
} else if (processor.name === 'unified') {
|
|
211
|
+
processor.options.rehypePlugins = [
|
|
212
|
+
...(processor.options.rehypePlugins ?? []).filter((plugin) => !isDocLinks(plugin)),
|
|
213
|
+
rehypeDocLinks(links),
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (config.docs.base === '') {
|
|
218
|
+
const ownIndex = INDEX_PAGE_NAMES.find((name) => existsSync(new URL(`pages/${name}`, astroConfig.srcDir)))
|
|
219
|
+
if (ownIndex) {
|
|
220
|
+
logger.warn(
|
|
221
|
+
`${ownIndex} owns the site root while JAAD docs are mounted there. ` +
|
|
222
|
+
'Set routeBase: "/docs" to keep a landing page and mount the docs below it.',
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const withoutTrailingSlash = (value: string) => (value.length > 1 ? value.replace(/\/$/, '') : value)
|
|
229
|
+
|
|
230
|
+
export function createSitemapFilter(
|
|
231
|
+
docsBase: string,
|
|
232
|
+
state: Pick<IntegrationState, 'deploymentBase' | 'aliases'>,
|
|
233
|
+
defaultLocale?: string,
|
|
234
|
+
): (page: string) => boolean {
|
|
235
|
+
return (page) => {
|
|
236
|
+
if (state.aliases.length === 0) return true
|
|
237
|
+
|
|
238
|
+
const pathname = withoutTrailingSlash(new URL(page).pathname)
|
|
239
|
+
return !state.aliases.some((alias) => {
|
|
240
|
+
const urls: DocsUrlOptions = {
|
|
241
|
+
docsBase,
|
|
242
|
+
deploymentBase: state.deploymentBase,
|
|
243
|
+
locale: alias.locale,
|
|
244
|
+
defaultLocale,
|
|
245
|
+
}
|
|
246
|
+
return pathname === withoutTrailingSlash(docsPageHref(alias.slug, urls))
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -10,13 +10,12 @@ type Props = DocsFrameProps
|
|
|
10
10
|
<div class="docs-container">
|
|
11
11
|
<slot name="sidebar" />
|
|
12
12
|
|
|
13
|
-
<div class="docs-main
|
|
13
|
+
<div class="docs-main">
|
|
14
14
|
<main class="docs-page-main">
|
|
15
15
|
<slot name="mobile-navigation" />
|
|
16
16
|
|
|
17
17
|
<article>
|
|
18
18
|
<slot name="breadcrumbs" />
|
|
19
|
-
<slot name="page-actions" />
|
|
20
19
|
<slot name="content" />
|
|
21
20
|
</article>
|
|
22
21
|
|
|
@@ -24,8 +23,11 @@ type Props = DocsFrameProps
|
|
|
24
23
|
</main>
|
|
25
24
|
</div>
|
|
26
25
|
|
|
27
|
-
<
|
|
28
|
-
|
|
26
|
+
<aside class="docs-sidebar-right jaad-sidebar">
|
|
27
|
+
<slot name="table-of-contents" />
|
|
28
|
+
<slot name="page-actions" />
|
|
29
|
+
</aside>
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
<slot name="footer" />
|
|
32
|
+
</div>
|
|
31
33
|
</div>
|
|
@@ -1,53 +1,53 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type { DocsEntry, DocsHeadings } from '../@types/docs.ts'
|
|
3
2
|
import type { DocsFrameProps } from '../advanced.ts'
|
|
3
|
+
import type { DocsSourceEntry } from '../collection.ts'
|
|
4
|
+
import type { DocsPage } from '../model/tree.ts'
|
|
4
5
|
import type { DocsNavigation } from '../navigation.ts'
|
|
6
|
+
import type { DocsHeading } from '../types.ts'
|
|
5
7
|
import { ClientRouter } from 'astro:transitions'
|
|
6
8
|
import config from 'virtual:jaad/config'
|
|
7
9
|
import DocsFrame from 'virtual:jaad/docs-frame'
|
|
10
|
+
import Footer from '../../site/components/Footer.astro'
|
|
11
|
+
import Head from '../../site/components/Head.astro'
|
|
12
|
+
import Header from '../../site/components/Header.astro'
|
|
13
|
+
import { DEFAULT_LOCALE, LOCALES } from '../../site/runtime.ts'
|
|
8
14
|
import { getLocaleAlternates } from '../collection.ts'
|
|
9
|
-
import DocsSidebarLeft from '../components/
|
|
10
|
-
import StructuredData from '../components/
|
|
11
|
-
import TableOfContents from '../components/
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import { docTitle, docTranslationKey, extractDescription, getCleanSlug, routedId } from '../utils/docs.ts'
|
|
17
|
-
import '../../dist/jaad.css'
|
|
15
|
+
import DocsSidebarLeft from '../components/DocsSidebarLeft.astro'
|
|
16
|
+
import StructuredData from '../components/StructuredData.astro'
|
|
17
|
+
import TableOfContents from '../components/TableOfContents.astro'
|
|
18
|
+
import { pageDescription } from '../model/rendered.ts'
|
|
19
|
+
import { docsUrls } from '../runtime.ts'
|
|
20
|
+
import { docsPageHref } from '../urls.ts'
|
|
21
|
+
import '../../../dist/jaad.css'
|
|
18
22
|
import '../styles/docs.css'
|
|
19
23
|
import '../styles/jaamd.css'
|
|
20
|
-
import '@jaamd/
|
|
24
|
+
import '@jaamd/astro/styles.css'
|
|
21
25
|
import 'virtual:jaad/theme.css'
|
|
22
26
|
import 'virtual:jaad/bridge.css'
|
|
23
27
|
import 'virtual:jaad/user.css'
|
|
24
28
|
import 'virtual:jaad/tailwind.css'
|
|
25
29
|
|
|
26
30
|
interface Props {
|
|
31
|
+
/** The `<title>` before the site name. Left out on the opening page. */
|
|
27
32
|
title?: string
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
headings?: DocsHeadings[]
|
|
33
|
+
page: DocsPage<DocsSourceEntry>
|
|
34
|
+
headings?: DocsHeading[]
|
|
31
35
|
navigation: DocsNavigation
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
const { title,
|
|
38
|
+
const { title, page, headings = [], navigation } = Astro.props
|
|
35
39
|
|
|
36
40
|
const pageTitle = title ? `${title} | ${config.title}` : config.title
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
const description =
|
|
43
|
+
page.data.description ?? pageDescription(page.source.rendered, page.title) ?? config.description ?? config.title
|
|
40
44
|
|
|
41
|
-
const
|
|
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
|
|
45
|
+
const locale = page.locale ?? DEFAULT_LOCALE
|
|
48
46
|
const language = LOCALES.find((known) => known.code === locale)?.tag ?? config.lang
|
|
49
47
|
|
|
50
|
-
const
|
|
48
|
+
const indexUrl = docsPageHref('search-index.json', docsUrls(locale))
|
|
49
|
+
|
|
50
|
+
const switcherAlternates = await getLocaleAlternates(page.translationKey)
|
|
51
51
|
|
|
52
52
|
// x-default is a landing place, so it takes the fallback when this page has no
|
|
53
53
|
// translation into the default locale.
|
|
@@ -64,11 +64,12 @@ const alternates = switcherAlternates
|
|
|
64
64
|
|
|
65
65
|
const frameProps: DocsFrameProps = {
|
|
66
66
|
page: {
|
|
67
|
-
id:
|
|
68
|
-
slug:
|
|
69
|
-
title:
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
id: page.id,
|
|
68
|
+
slug: page.slug,
|
|
69
|
+
title: page.title,
|
|
70
|
+
section: page.section,
|
|
71
|
+
chapter: page.chapter,
|
|
72
|
+
data: page.data,
|
|
72
73
|
},
|
|
73
74
|
headings,
|
|
74
75
|
navigation,
|
|
@@ -80,22 +81,22 @@ const frameProps: DocsFrameProps = {
|
|
|
80
81
|
<head>
|
|
81
82
|
<Head
|
|
82
83
|
title={pageTitle}
|
|
83
|
-
description={
|
|
84
|
-
keywords={
|
|
85
|
-
author={
|
|
86
|
-
image={
|
|
87
|
-
modified={
|
|
84
|
+
description={description}
|
|
85
|
+
keywords={page.data.keywords}
|
|
86
|
+
author={page.data.author}
|
|
87
|
+
image={page.data.ogImage}
|
|
88
|
+
modified={page.data.lastUpdated}
|
|
88
89
|
language={language}
|
|
89
90
|
{alternates}
|
|
90
91
|
{xDefault}
|
|
91
92
|
/>
|
|
92
93
|
<StructuredData
|
|
93
|
-
title={
|
|
94
|
-
description={
|
|
95
|
-
chapter={chapter}
|
|
96
|
-
keywords={
|
|
97
|
-
author={
|
|
98
|
-
modified={
|
|
94
|
+
title={page.title || config.title}
|
|
95
|
+
description={description}
|
|
96
|
+
chapter={page.chapter}
|
|
97
|
+
keywords={page.data.keywords}
|
|
98
|
+
author={page.data.author}
|
|
99
|
+
modified={page.data.lastUpdated}
|
|
99
100
|
{language}
|
|
100
101
|
{locale}
|
|
101
102
|
/>
|
|
@@ -103,8 +104,8 @@ const frameProps: DocsFrameProps = {
|
|
|
103
104
|
</head>
|
|
104
105
|
<body data-docs class="jaad-body">
|
|
105
106
|
<DocsFrame {...frameProps}>
|
|
106
|
-
<Header slot="header"
|
|
107
|
-
<DocsSidebarLeft slot="sidebar"
|
|
107
|
+
<Header slot="header" {locale} alternates={switcherAlternates} search={{ indexUrl, prominent: true }} />
|
|
108
|
+
<DocsSidebarLeft slot="sidebar" groups={navigation.groups} {locale} />
|
|
108
109
|
<slot name="mobile-navigation" slot="mobile-navigation" />
|
|
109
110
|
<slot name="breadcrumbs" slot="breadcrumbs" />
|
|
110
111
|
<slot name="page-actions" slot="page-actions" />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Shell from '../../site/layouts/Shell.astro'
|
|
4
|
+
import { DEFAULT_LOCALE } from '../../site/runtime.ts'
|
|
5
|
+
import { getLocaleAlternates } from '../collection.ts'
|
|
6
|
+
import { docsUrls } from '../runtime.ts'
|
|
7
|
+
import { docsPageHref } from '../urls.ts'
|
|
8
|
+
|
|
9
|
+
type Props = Omit<ComponentProps<typeof Shell>, 'header'>
|
|
10
|
+
|
|
11
|
+
// A page of the project's own has no translations: offer each locale's opener.
|
|
12
|
+
const alternates = await getLocaleAlternates('')
|
|
13
|
+
const indexUrl = docsPageHref('search-index.json', docsUrls(DEFAULT_LOCALE))
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<Shell {...Astro.props} header={{ alternates, search: { indexUrl } }}>
|
|
17
|
+
<slot />
|
|
18
|
+
</Shell>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DocLinkContext } from './links.ts'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { resolveDocLink } from './links.ts'
|
|
4
|
+
|
|
5
|
+
interface HastNode {
|
|
6
|
+
type: string
|
|
7
|
+
tagName?: string
|
|
8
|
+
properties?: Record<string, unknown>
|
|
9
|
+
children?: HastNode[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** rehype: rewrites relative links to markdown files into page urls. */
|
|
13
|
+
export function rehypeDocLinks(context: DocLinkContext) {
|
|
14
|
+
return function jaadDocLinks() {
|
|
15
|
+
return (tree: HastNode, file: { path?: string }) => {
|
|
16
|
+
const from = file.path
|
|
17
|
+
if (!from) return
|
|
18
|
+
|
|
19
|
+
const visit = (node: HastNode) => {
|
|
20
|
+
const href = node.tagName === 'a' ? node.properties?.href : undefined
|
|
21
|
+
if (typeof href === 'string') {
|
|
22
|
+
const resolved = resolveDocLink(href, from, context)
|
|
23
|
+
if (resolved) node.properties!.href = resolved
|
|
24
|
+
}
|
|
25
|
+
node.children?.forEach(visit)
|
|
26
|
+
}
|
|
27
|
+
visit(tree)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SatteriContext {
|
|
33
|
+
fileURL: URL | undefined
|
|
34
|
+
setProperty(node: HastNode, name: string, value: string): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Sätteri: rewrites relative links to markdown files into page urls. */
|
|
38
|
+
export function satteriDocLinks(context: DocLinkContext) {
|
|
39
|
+
return {
|
|
40
|
+
name: 'jaad-doc-links',
|
|
41
|
+
element: {
|
|
42
|
+
filter: ['a'],
|
|
43
|
+
visit(node: HastNode, ctx: SatteriContext) {
|
|
44
|
+
const href = node.properties?.href
|
|
45
|
+
if (typeof href !== 'string' || !ctx.fileURL) return
|
|
46
|
+
|
|
47
|
+
const resolved = resolveDocLink(href, fileURLToPath(ctx.fileURL), context)
|
|
48
|
+
if (resolved) ctx.setProperty(node, 'href', resolved)
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DocsRoot } from './config.ts'
|
|
2
|
+
import type { DocsUrlOptions } from './urls.ts'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { dirname, relative, resolve, sep } from 'node:path'
|
|
5
|
+
import { getDocCollectionId, namedSlug, splitDocLocale } from './model/ids.ts'
|
|
6
|
+
import { docsPageHref } from './urls.ts'
|
|
7
|
+
|
|
8
|
+
export interface DocLinkContext {
|
|
9
|
+
roots: DocsRoot[]
|
|
10
|
+
locales: string[]
|
|
11
|
+
urls: (locale?: string) => DocsUrlOptions
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The url of the page a relative link to a markdown file points at, or null
|
|
16
|
+
* when the link is not one: absolute, external, or to a file outside the docs.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveDocLink(href: string, fromFile: string, context: DocLinkContext): string | null {
|
|
19
|
+
if (/^([a-z][a-z\d+.-]*:|\/|#)/i.test(href)) return null
|
|
20
|
+
|
|
21
|
+
const [path, hash = ''] = href.split('#', 2)
|
|
22
|
+
if (!path.endsWith('.md')) return null
|
|
23
|
+
|
|
24
|
+
let decoded: string
|
|
25
|
+
try {
|
|
26
|
+
decoded = decodeURI(path)
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const target = resolve(dirname(fromFile), decoded)
|
|
32
|
+
const root = context.roots.find(({ dir }) => target.startsWith(dir + sep))
|
|
33
|
+
if (!root) return null
|
|
34
|
+
|
|
35
|
+
if (!existsSync(target)) {
|
|
36
|
+
throw new Error(`jaad: ${fromFile} links to ${href}, which does not exist`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { locale, id } = splitDocLocale(getDocCollectionId(relative(root.dir, target)), context.locales)
|
|
40
|
+
const slug = namedSlug(id, root.section)
|
|
41
|
+
return docsPageHref(slug, context.urls(locale ?? undefined)) + (hash ? `#${hash}` : '')
|
|
42
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { isLocale } from '
|
|
3
|
+
import { isLocale } from '../site/locales.ts'
|
|
4
4
|
|
|
5
5
|
function hasMarkdown(dir: string): boolean {
|
|
6
6
|
return readdirSync(dir, { withFileTypes: true }).some((entry) =>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { slug as githubSlug } from 'github-slugger'
|
|
2
|
+
import { ucfirst } from '../../site/helpers.ts'
|
|
3
|
+
|
|
4
|
+
/** A numeric order prefix, e.g. "01-" or "123-". */
|
|
5
|
+
const NUMBERED_PREFIX_RE = /^(\d+)-(.+)$/
|
|
6
|
+
|
|
7
|
+
/** What a page's path says: its file order, its chapter and its own name. */
|
|
8
|
+
export interface ParsedDocPath {
|
|
9
|
+
/** The file name without its order prefix. */
|
|
10
|
+
slug: string
|
|
11
|
+
order: number
|
|
12
|
+
/** The chapter directory without its order prefix. */
|
|
13
|
+
chapter?: string
|
|
14
|
+
orderChapter?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseDocPath(path: string): ParsedDocPath {
|
|
18
|
+
const parts = path.split('/')
|
|
19
|
+
const name = NUMBERED_PREFIX_RE.exec(parts[parts.length - 1])
|
|
20
|
+
const directory = parts.length >= 2 ? parts[parts.length - 2] : undefined
|
|
21
|
+
const chapter = directory ? NUMBERED_PREFIX_RE.exec(directory) : null
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
slug: name ? name[2] : parts[parts.length - 1],
|
|
25
|
+
order: name ? parseInt(name[1], 10) : 999,
|
|
26
|
+
chapter: chapter ? chapter[2] : directory,
|
|
27
|
+
orderChapter: chapter ? parseInt(chapter[1], 10) : undefined,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** "01-chapter/02-section" → "chapter/section" */
|
|
32
|
+
export function getCleanSlug(id: string): string {
|
|
33
|
+
return id
|
|
34
|
+
.split('/')
|
|
35
|
+
.map((part) => part.replace(NUMBERED_PREFIX_RE, '$2'))
|
|
36
|
+
.join('/')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A page's named url below the docs mount: its section, then its clean path. */
|
|
40
|
+
export function namedSlug(path: string, section?: string): string {
|
|
41
|
+
const slug = getCleanSlug(path)
|
|
42
|
+
return section ? `${section}/${slug}` : slug
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** "it/02-guides/01-setup" becomes locale "it" and "02-guides/01-setup". */
|
|
46
|
+
export function splitDocLocale(id: string, locales: string[]): { locale: string | null; id: string } {
|
|
47
|
+
const slash = id.indexOf('/')
|
|
48
|
+
if (slash === -1) return { locale: null, id }
|
|
49
|
+
|
|
50
|
+
const first = id.slice(0, slash)
|
|
51
|
+
if (!locales.includes(first)) return { locale: null, id }
|
|
52
|
+
return { locale: first, id: id.slice(slash + 1) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Match the ids produced by Astro's glob loader, independently of the OS. */
|
|
56
|
+
export function getDocCollectionId(filePath: string): string {
|
|
57
|
+
return filePath
|
|
58
|
+
.replace(/\.md$/, '')
|
|
59
|
+
.split(/[\\/]/)
|
|
60
|
+
.map((part) => githubSlug(part))
|
|
61
|
+
.join('/')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The collection holding the docs, or the pages of one section. */
|
|
65
|
+
export function collectionName(section?: string): string {
|
|
66
|
+
return section ? `docsPages-${section}` : 'docsPages'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The id of a file given relative to its docs directory: its locale, the
|
|
70
|
+
* section that directory is, then its path. */
|
|
71
|
+
export function collectionIdFor(filePath: string, section: string | undefined, locales: string[]): string {
|
|
72
|
+
const { locale, id } = splitDocLocale(getDocCollectionId(filePath), locales)
|
|
73
|
+
return [locale, section, id].filter(Boolean).join('/')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Decodes percent-escapes, so "detail_%26_summary" → "Detail & Summary". */
|
|
77
|
+
export function slugToTitle(slug: string): string {
|
|
78
|
+
return slug
|
|
79
|
+
.split(/[-_]+/)
|
|
80
|
+
.map((word) => ucfirst(decodeURIComponent(word)))
|
|
81
|
+
.join(' ')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatChapterTitle(chapter: string | undefined): string | null {
|
|
85
|
+
if (!chapter) return null
|
|
86
|
+
return slugToTitle(chapter)
|
|
87
|
+
}
|