@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.
Files changed (138) hide show
  1. package/README.md +8 -2
  2. package/dist/jaad.css +232 -347
  3. package/docs/00-overview.md +25 -0
  4. package/docs/01-getting-started/01-installation.md +216 -0
  5. package/docs/01-getting-started/02-pages.md +109 -0
  6. package/docs/01-getting-started/03-deployment.md +100 -0
  7. package/docs/02-configurations/01-reference.md +215 -0
  8. package/docs/02-configurations/02-frontmatter.md +105 -0
  9. package/docs/02-configurations/03-i18n.md +210 -0
  10. package/docs/02-configurations/04-search.md +58 -0
  11. package/docs/02-configurations/05-themes.md +104 -0
  12. package/docs/02-configurations/06-styles.md +156 -0
  13. package/docs/02-configurations/07-custom-pages.md +119 -0
  14. package/docs/02-configurations/08-advanced-layout.md +139 -0
  15. package/docs/03-upgrading.md +208 -0
  16. package/index.ts +8 -22
  17. package/package.json +10 -9
  18. package/site.ts +18 -1
  19. package/src/config.ts +25 -176
  20. package/src/{advanced.ts → docs/advanced.ts} +6 -4
  21. package/src/docs/collection.ts +109 -0
  22. package/src/docs/components/Breadcrumbs.astro +29 -0
  23. package/src/docs/components/DocsMobileNav.astro +90 -0
  24. package/src/docs/components/DocsNavTree.astro +57 -0
  25. package/src/docs/components/DocsPage.astro +66 -0
  26. package/src/{components/docs → docs/components}/DocsPageNav.astro +4 -4
  27. package/src/docs/components/DocsSidebarLeft.astro +53 -0
  28. package/src/{components/docs → docs/components}/PageActions.astro +10 -8
  29. package/src/{components/docs → docs/components}/StructuredData.astro +3 -3
  30. package/src/{components/docs → docs/components}/TableOfContents.astro +20 -22
  31. package/src/docs/config.ts +167 -0
  32. package/src/docs/content.ts +36 -0
  33. package/src/docs/endpoints.ts +89 -0
  34. package/src/docs/integration.ts +249 -0
  35. package/src/{layouts → docs/layouts}/DefaultDocsFrame.astro +7 -5
  36. package/src/{layouts → docs/layouts}/Docs.astro +44 -43
  37. package/src/docs/layouts/Page.astro +18 -0
  38. package/src/docs/link-plugins.ts +52 -0
  39. package/src/docs/links.ts +42 -0
  40. package/src/{locale-tree.ts → docs/locale-tree.ts} +1 -1
  41. package/src/docs/model/ids.ts +87 -0
  42. package/src/docs/model/rendered.ts +185 -0
  43. package/src/docs/model/tree.ts +124 -0
  44. package/src/docs/model/validate.ts +75 -0
  45. package/src/docs/navigation.ts +75 -0
  46. package/src/{routes → docs/routes}/404.astro +1 -1
  47. package/src/docs/routes/docs-index.astro +19 -0
  48. package/src/docs/routes/docs-slug.astro +54 -0
  49. package/src/{routes → docs/routes}/docs-slug.md.ts +8 -10
  50. package/src/{routes → docs/routes}/llms-locale.txt.ts +1 -1
  51. package/src/{routes → docs/routes}/llms.txt.ts +1 -1
  52. package/src/{routes → docs/routes}/search-index-locale.json.ts +1 -1
  53. package/src/{routes → docs/routes}/search-index.json.ts +1 -1
  54. package/src/docs/runtime.ts +17 -0
  55. package/src/docs/search-index.ts +39 -0
  56. package/src/docs/styles/components/breadcrumbs.css +26 -0
  57. package/src/docs/styles/components/docs-mobile-nav.css +47 -0
  58. package/src/docs/styles/components/docs-page-nav.css +41 -0
  59. package/src/docs/styles/components/docs-sidebar.css +59 -0
  60. package/src/docs/styles/components/page-actions.css +51 -0
  61. package/src/docs/styles/components/toc.css +38 -0
  62. package/src/docs/styles/components.css +7 -0
  63. package/src/docs/styles/docs.css +81 -0
  64. package/src/{@types/docs.ts → docs/types.ts} +8 -16
  65. package/src/{urls.ts → docs/urls.ts} +2 -16
  66. package/src/env.d.ts +1 -1
  67. package/src/integration.ts +15 -249
  68. package/src/{components → site/components}/Footer.astro +1 -1
  69. package/src/{components → site/components}/Header.astro +14 -11
  70. package/src/{components → site/components}/LocaleSwitcher.astro +2 -2
  71. package/src/{components → site/components}/NavMobile.astro +1 -1
  72. package/src/{components → site/components}/SearchPalette.astro +160 -79
  73. package/src/{components → site/components}/ThemeSwitcher.astro +1 -1
  74. package/src/site/config.ts +149 -0
  75. package/src/{infer.ts → site/infer.ts} +3 -3
  76. package/src/site/integration.ts +59 -0
  77. package/src/{layouts/Page.astro → site/layouts/Shell.astro} +6 -3
  78. package/src/{locales.ts → site/locales.ts} +9 -1
  79. package/src/{docs-config.ts → site/runtime.ts} +1 -12
  80. package/src/site/search.ts +227 -0
  81. package/src/{social.ts → site/social.ts} +2 -2
  82. package/src/site/styles/components/header.css +31 -0
  83. package/src/{styles → site/styles}/components/search.css +21 -1
  84. package/src/{styles → site/styles}/components/shell.css +4 -0
  85. package/src/site/styles/components.css +10 -0
  86. package/src/{styles → site/styles}/global.css +4 -26
  87. package/src/{themes/index.ts → site/themes.ts} +1 -1
  88. package/src/{ui → site/ui}/en.ts +3 -5
  89. package/src/{ui → site/ui}/it.ts +2 -4
  90. package/src/site/urls.ts +14 -0
  91. package/src/{virtual.ts → site/virtual.ts} +12 -18
  92. package/src/styles.css +5 -0
  93. package/src/collection.ts +0 -97
  94. package/src/components/ScrollToTop.astro +0 -43
  95. package/src/components/docs/Breadcrumbs.astro +0 -36
  96. package/src/components/docs/DocsMobileNav.astro +0 -93
  97. package/src/components/docs/DocsPage.astro +0 -68
  98. package/src/components/docs/DocsSidebarLeft.astro +0 -82
  99. package/src/content.ts +0 -42
  100. package/src/navigation.ts +0 -97
  101. package/src/routes/docs-index.astro +0 -19
  102. package/src/routes/docs-slug.astro +0 -58
  103. package/src/styles/components/breadcrumbs.css +0 -13
  104. package/src/styles/components/docs-mobile-nav.css +0 -9
  105. package/src/styles/components/docs-page-nav.css +0 -31
  106. package/src/styles/components/docs-sidebar.css +0 -42
  107. package/src/styles/components/header.css +0 -20
  108. package/src/styles/components/page-actions.css +0 -13
  109. package/src/styles/components/scroll-to-top.css +0 -11
  110. package/src/styles/components/toc.css +0 -21
  111. package/src/styles/docs.css +0 -112
  112. package/src/utils/docs.ts +0 -300
  113. package/src/utils/endpoints.ts +0 -80
  114. package/src/utils/search.ts +0 -83
  115. /package/src/{styles → docs/styles}/components/docs-page.css +0 -0
  116. /package/src/{styles → docs/styles}/jaamd.css +0 -0
  117. /package/src/{components → site/components}/Head.astro +0 -0
  118. /package/src/{components → site/components}/Nav.astro +0 -0
  119. /package/src/{components → site/components}/SocialLinks.astro +0 -0
  120. /package/src/{components → site/components}/locale-switcher.ts +0 -0
  121. /package/src/{fonts → site/fonts}/OFL.txt +0 -0
  122. /package/src/{fonts → site/fonts}/inter.woff2 +0 -0
  123. /package/src/{fonts → site/fonts}/merriweather-italic.woff2 +0 -0
  124. /package/src/{fonts → site/fonts}/merriweather.woff2 +0 -0
  125. /package/src/{fonts.ts → site/fonts.ts} +0 -0
  126. /package/src/{forges.ts → site/forges.ts} +0 -0
  127. /package/src/{utils → site}/helpers.ts +0 -0
  128. /package/src/{styles → site/styles}/components/footer.css +0 -0
  129. /package/src/{styles → site/styles}/components/locale-switcher.css +0 -0
  130. /package/src/{styles → site/styles}/components/nav-mobile.css +0 -0
  131. /package/src/{styles → site/styles}/components/nav.css +0 -0
  132. /package/src/{styles → site/styles}/components/not-found.css +0 -0
  133. /package/src/{styles → site/styles}/components/social-links.css +0 -0
  134. /package/src/{styles → site/styles}/components/theme-switcher.css +0 -0
  135. /package/src/{styles → site/styles}/jaamd-forward.css +0 -0
  136. /package/src/{styles → site/styles}/jaamd-reverse.css +0 -0
  137. /package/src/{styles → site/styles}/tailwind.css +0 -0
  138. /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 config from 'virtual:jaad/config'
3
- import { docsUrls, ui } from '../../docs-config.ts'
4
- import { editUrl } from '../../infer.ts'
5
- import { docsMarkdownHref } from '../../urls.ts'
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
- /** Content collection entry id, e.g. "03-markdown/12-inner-html". */
9
- id: string
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 { id, cleanSlug, locale } = Astro.props
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 = config.editBase ? editUrl(config.editBase, `${id}.md`) : null
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 { docsUrls } from '../../docs-config.ts'
4
- import { docsPageHref } from '../../urls.ts'
5
- import { formatChapterTitle } from '../../utils/docs.ts'
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 { DocsHeadings } from '../../@types/docs.ts'
3
- import { ui } from '../../docs-config.ts'
2
+ import type { DocsHeading } from '../types.ts'
3
+ import { ui } from '../../site/runtime.ts'
4
4
 
5
5
  interface Props {
6
- headings: DocsHeadings[]
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
- <aside id="toc-sidebar" class="docs-sidebar-right jaad-sidebar">
16
- <nav aria-label={t('toc.label')}>
17
- <h2 class="docs-toc-title">{t('toc.title')}</h2>
18
- <ul class="docs-toc-list">
19
- {headings.map((heading) => (
20
- <li>
21
- <a
22
- href={`#${heading.slug}`}
23
- data-toc-link
24
- class:list={['docs-toc-link', heading.depth === 3 && 'docs-toc-link-nested']}
25
- >
26
- {heading.text}
27
- </a>
28
- </li>
29
- ))}
30
- </ul>
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-sidebar')
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
+ }