@jaamd/jaad 0.11.0 → 1.0.0-rc.1
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
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
@layer components {
|
|
2
|
-
.jaad-scroll-top-button {
|
|
3
|
-
@apply bg-background text-foreground hover:bg-background/80 border-foreground/20 pointer-events-none fixed right-6 bottom-6 z-50 flex h-12 w-12 items-center justify-center rounded-full border opacity-0 shadow-lg transition-opacity duration-300 lg:hidden;
|
|
4
|
-
}
|
|
5
|
-
.jaad-scroll-top-button[data-visible] {
|
|
6
|
-
@apply pointer-events-auto opacity-100;
|
|
7
|
-
}
|
|
8
|
-
.jaad-scroll-top-icon {
|
|
9
|
-
@apply h-6 w-6;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
@layer components {
|
|
2
|
-
.docs-toc-title {
|
|
3
|
-
@apply text-primary/60 mb-4 text-xs font-semibold tracking-wider uppercase;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.docs-toc-list {
|
|
7
|
-
@apply space-y-2;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.docs-toc-link {
|
|
11
|
-
@apply text-foreground/60 hover:text-primary block border-l-2 border-transparent py-1 pl-3 text-xs no-underline transition-colors;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.docs-toc-link-nested {
|
|
15
|
-
@apply pl-6;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.docs-toc-link[aria-current='location'] {
|
|
19
|
-
@apply border-primary;
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/styles/docs.css
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/* Layered, so a page's own utilities still win. */
|
|
2
|
-
@layer components {
|
|
3
|
-
.docs-frame {
|
|
4
|
-
display: contents;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.docs-container {
|
|
8
|
-
position: relative;
|
|
9
|
-
width: 100%;
|
|
10
|
-
min-height: calc(100vh - 6.5rem);
|
|
11
|
-
display: flex;
|
|
12
|
-
justify-content: center;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/* The inline margin is deliberate and survives every breakpoint: it sits
|
|
16
|
-
outside the page padding, which only starts at 48rem. */
|
|
17
|
-
.docs-page-main {
|
|
18
|
-
flex: 1;
|
|
19
|
-
margin-block: 2rem;
|
|
20
|
-
margin-inline: 1rem;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@media (min-width: 48rem) {
|
|
24
|
-
.docs-page-main {
|
|
25
|
-
margin-block: 4rem;
|
|
26
|
-
padding-inline: var(--jaad-page-padding);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@media (min-width: 64rem) {
|
|
31
|
-
.docs-page-main {
|
|
32
|
-
margin-bottom: 8rem;
|
|
33
|
-
padding-inline: calc(var(--jaad-page-padding) * 2);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.docs-main {
|
|
38
|
-
width: 100%;
|
|
39
|
-
max-width: var(--jaad-content-width);
|
|
40
|
-
position: relative;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* The room the sidebars need, so widening them widens the gutters too. */
|
|
44
|
-
@media (min-width: 64rem) {
|
|
45
|
-
.docs-main-gutters {
|
|
46
|
-
margin-inline: var(--jaad-sidebar-width);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
@media (min-width: 64rem) {
|
|
51
|
-
[data-jaad-default-frame] .docs-sidebar-left,
|
|
52
|
-
[data-jaad-default-frame] .docs-sidebar-right {
|
|
53
|
-
position: fixed;
|
|
54
|
-
top: 0;
|
|
55
|
-
height: 100vh;
|
|
56
|
-
padding: 6rem 1.5rem 2rem;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* max() keeps the sidebar on screen when the centred offset would be
|
|
60
|
-
negative, whatever the content and sidebar tokens are set to. */
|
|
61
|
-
[data-jaad-default-frame] .docs-sidebar-left {
|
|
62
|
-
left: max(1rem, var(--jaad-sidebar-offset));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
[data-jaad-default-frame] .docs-sidebar-right {
|
|
66
|
-
right: max(1rem, var(--jaad-sidebar-offset));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
@media (width < 64rem) {
|
|
71
|
-
[data-jaad-default-frame] .docs-sidebar-left,
|
|
72
|
-
[data-jaad-default-frame] .docs-sidebar-right {
|
|
73
|
-
display: none;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* A mask, so the colour comes from the token and follows the theme.
|
|
78
|
-
Override --jaad-select-arrow for a different shape. */
|
|
79
|
-
.docs-select {
|
|
80
|
-
position: relative;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.docs-select::after {
|
|
84
|
-
content: '';
|
|
85
|
-
position: absolute;
|
|
86
|
-
top: 50%;
|
|
87
|
-
right: 0.75rem;
|
|
88
|
-
width: 1.25rem;
|
|
89
|
-
height: 1.25rem;
|
|
90
|
-
transform: translateY(-50%);
|
|
91
|
-
pointer-events: none;
|
|
92
|
-
background-color: var(--color-foreground);
|
|
93
|
-
mask: var(
|
|
94
|
-
--jaad-select-arrow,
|
|
95
|
-
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' viewBox='0 0 20 20'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E")
|
|
96
|
-
)
|
|
97
|
-
center / contain no-repeat;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.docs-mobile-nav optgroup {
|
|
101
|
-
font-weight: 600;
|
|
102
|
-
color: var(--color-primary);
|
|
103
|
-
background: var(--color-surface);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.docs-mobile-nav option {
|
|
107
|
-
font-weight: 400;
|
|
108
|
-
color: var(--color-foreground);
|
|
109
|
-
background: var(--color-surface);
|
|
110
|
-
padding: 0.5rem;
|
|
111
|
-
}
|
|
112
|
-
}
|
package/src/utils/docs.ts
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import type { DocsHeadings, DocsPageData, ParsedDocsCollectionId } from '../@types/docs.ts'
|
|
2
|
-
import GithubSlugger, { slug as githubSlug } from 'github-slugger'
|
|
3
|
-
import { ucfirst } from './helpers.ts'
|
|
4
|
-
|
|
5
|
-
/** The id without its locale directory: what routing and ordering read. */
|
|
6
|
-
export function routedId(page: DocsPageLike): string {
|
|
7
|
-
return page.localeId ?? page.id
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** "it/02-guides/01-setup" becomes locale "it" and "02-guides/01-setup". */
|
|
11
|
-
export function splitDocLocale(id: string, locales: string[]): { locale: string | null; id: string } {
|
|
12
|
-
const slash = id.indexOf('/')
|
|
13
|
-
if (slash === -1) return { locale: null, id }
|
|
14
|
-
|
|
15
|
-
const first = id.slice(0, slash)
|
|
16
|
-
if (!locales.includes(first)) return { locale: null, id }
|
|
17
|
-
return { locale: first, id: id.slice(slash + 1) }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Matches a numeric order prefix, e.g. "01-" or "123-" */
|
|
21
|
-
const NUMBERED_PREFIX_RE = /^(\d+)-(.+)$/
|
|
22
|
-
|
|
23
|
-
export function parseDocCollectionId(id: string): ParsedDocsCollectionId {
|
|
24
|
-
let order = 999
|
|
25
|
-
let orderChapter: number | undefined
|
|
26
|
-
let chapter: string | undefined
|
|
27
|
-
|
|
28
|
-
const parts = id.split('/')
|
|
29
|
-
let slug: string = parts[parts.length - 1]
|
|
30
|
-
|
|
31
|
-
if (parts.length >= 2) {
|
|
32
|
-
const chapterPart = parts[parts.length - 2]
|
|
33
|
-
const chapterMatch = NUMBERED_PREFIX_RE.exec(chapterPart)
|
|
34
|
-
if (chapterMatch) {
|
|
35
|
-
orderChapter = parseInt(chapterMatch[1], 10)
|
|
36
|
-
chapter = chapterMatch[2]
|
|
37
|
-
} else {
|
|
38
|
-
chapter = chapterPart
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const match = NUMBERED_PREFIX_RE.exec(slug)
|
|
43
|
-
if (match) {
|
|
44
|
-
order = parseInt(match[1], 10)
|
|
45
|
-
slug = match[2]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { order, orderChapter, chapter, slug, title: slugToTitle(slug) }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Chapter order first, then file order within it. Root pages interleave with
|
|
52
|
-
* chapters by their own number. */
|
|
53
|
-
export function sortDocPages<T extends DocsPageLike>(pages: T[]): T[] {
|
|
54
|
-
return pages
|
|
55
|
-
.map((page) => ({ page, ...docOrder(page) }))
|
|
56
|
-
.toSorted((a, b) => a.primary - b.primary || a.secondary - b.secondary)
|
|
57
|
-
.map((entry) => entry.page)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Frontmatter `order` replaces the filename prefix number. A chapter keeps
|
|
61
|
-
* its own. */
|
|
62
|
-
export function docOrder(page: DocsPageLike): {
|
|
63
|
-
primary: number
|
|
64
|
-
secondary: number
|
|
65
|
-
} {
|
|
66
|
-
const parsed = parseDocCollectionId(routedId(page))
|
|
67
|
-
const order = page.data?.order ?? parsed.order
|
|
68
|
-
return { primary: parsed.orderChapter ?? order, secondary: order }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** "01-chapter/02-section" → "chapter/section" */
|
|
72
|
-
export function getCleanSlug(id: string): string {
|
|
73
|
-
return id
|
|
74
|
-
.split('/')
|
|
75
|
-
.map((part) => part.replace(NUMBERED_PREFIX_RE, '$2'))
|
|
76
|
-
.join('/')
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Match the ids produced by Astro's glob loader, independently of the OS. */
|
|
80
|
-
export function getDocCollectionId(filePath: string): string {
|
|
81
|
-
return filePath
|
|
82
|
-
.replace(/\.md$/, '')
|
|
83
|
-
.split(/[\\/]/)
|
|
84
|
-
.map((part) => githubSlug(part))
|
|
85
|
-
.join('/')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Fail before route generation when the file tree cannot produce one clear
|
|
89
|
-
* navigation and URL for every page. */
|
|
90
|
-
export function validateDocsStructure(pages: DocsPageLike[], locale?: string): void {
|
|
91
|
-
const idsBySlug = new Map<string, string[]>()
|
|
92
|
-
const directoriesByChapter = new Map<string, Set<string>>()
|
|
93
|
-
const issues: string[] = []
|
|
94
|
-
// Messages name the file on disk, locale directory included.
|
|
95
|
-
const named = (id: string) => (locale ? `${locale}/${id}` : id)
|
|
96
|
-
|
|
97
|
-
for (const id of pages.map(routedId).toSorted()) {
|
|
98
|
-
const parts = id.split('/')
|
|
99
|
-
if (parts.length > 2) {
|
|
100
|
-
issues.push(`${named(id)}: only one chapter directory is supported inside docs/`)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const slug = getCleanSlug(id)
|
|
104
|
-
const ids = idsBySlug.get(slug) ?? []
|
|
105
|
-
ids.push(id)
|
|
106
|
-
idsBySlug.set(slug, ids)
|
|
107
|
-
|
|
108
|
-
if (parts.length === 2) {
|
|
109
|
-
const directory = parts[0]
|
|
110
|
-
const chapter = getCleanSlug(directory)
|
|
111
|
-
const directories = directoriesByChapter.get(chapter) ?? new Set()
|
|
112
|
-
directories.add(directory)
|
|
113
|
-
directoriesByChapter.set(chapter, directories)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
for (const [slug, ids] of idsBySlug) {
|
|
118
|
-
if (ids.length > 1) {
|
|
119
|
-
issues.push(`${ids.map(named).join(', ')} all resolve to /${slug}`)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const byKey = new Map<string, string[]>()
|
|
124
|
-
for (const page of pages) {
|
|
125
|
-
const key = page.data?.translationKey
|
|
126
|
-
if (!key) continue
|
|
127
|
-
const ids = byKey.get(key) ?? []
|
|
128
|
-
ids.push(routedId(page))
|
|
129
|
-
byKey.set(key, ids)
|
|
130
|
-
}
|
|
131
|
-
for (const [key, ids] of byKey) {
|
|
132
|
-
if (ids.length > 1) {
|
|
133
|
-
issues.push(`${ids.map(named).join(', ')} all claim translationKey ${key}`)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
for (const [chapter, directories] of directoriesByChapter) {
|
|
138
|
-
if (directories.size > 1) {
|
|
139
|
-
issues.push(`${[...directories].map(named).join(', ')} all resolve to chapter /${chapter}`)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (issues.length > 0) {
|
|
144
|
-
throw new Error(`jaad: invalid documentation structure\n${issues.map((issue) => ` ${issue}`).join('\n')}`)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** The opening page owns routeBase; every later page keeps its named slug. */
|
|
149
|
-
export function getCanonicalDocSlug(page: DocsPageLike, index: number): string {
|
|
150
|
-
return index === 0 ? '' : getCleanSlug(routedId(page))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Decodes percent-escapes, so "detail_%26_summary" → "Detail & Summary". */
|
|
154
|
-
export function slugToTitle(slug: string): string {
|
|
155
|
-
return slug
|
|
156
|
-
.split(/[-_]+/)
|
|
157
|
-
.map((word) => ucfirst(decodeURIComponent(word)))
|
|
158
|
-
.join(' ')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function formatChapterTitle(chapter: string | undefined): string | null {
|
|
162
|
-
if (!chapter) return null
|
|
163
|
-
return slugToTitle(chapter)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** The authoritative title: Astro's glob loader strips special characters from
|
|
167
|
-
* filenames, so a slug-derived title is lossy. */
|
|
168
|
-
export function extractTitleFromMarkdown(body: string): string | null {
|
|
169
|
-
for (const line of body.split('\n')) {
|
|
170
|
-
const match = line.match(/^#\s+(.+)$/)
|
|
171
|
-
if (match) return match[1].trim()
|
|
172
|
-
}
|
|
173
|
-
return null
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Skips fenced blocks, so headings written as code samples stay out of the TOC. */
|
|
177
|
-
/** Opening minus closing `tag` elements on one line. */
|
|
178
|
-
function tagBalance(line: string, tag: string): number {
|
|
179
|
-
const count = (re: RegExp) => line.match(re)?.length ?? 0
|
|
180
|
-
return count(new RegExp(`<${tag}[\\s>]`, 'gi')) - count(new RegExp(`</${tag}>`, 'gi'))
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function extractHeadingsFromMarkdown(markdown: string): DocsHeadings[] {
|
|
184
|
-
const headings: DocsHeadings[] = []
|
|
185
|
-
// Per page, like the renderer: repeated headings get -1, -2 suffixes.
|
|
186
|
-
const slugger = new GithubSlugger()
|
|
187
|
-
let insideFence = false
|
|
188
|
-
// The open `data-toc-ignore` element, if any.
|
|
189
|
-
let ignored: { tag: string; depth: number } | null = null
|
|
190
|
-
|
|
191
|
-
for (const line of markdown.split('\n')) {
|
|
192
|
-
if (line.match(/^(`{3,}|~{3,})/)) {
|
|
193
|
-
insideFence = !insideFence
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
if (insideFence) continue
|
|
197
|
-
|
|
198
|
-
if (ignored) {
|
|
199
|
-
ignored.depth += tagBalance(line, ignored.tag)
|
|
200
|
-
if (ignored.depth <= 0) ignored = null
|
|
201
|
-
} else {
|
|
202
|
-
const tag = line.match(/<([a-z][\w-]*)[^>]*\sdata-toc-ignore\b/i)?.[1]
|
|
203
|
-
const depth = tag ? tagBalance(line, tag) : 0
|
|
204
|
-
if (tag && depth > 0) ignored = { tag, depth }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const match = line.match(/^(#{2,3})\s+(.+)$/)
|
|
208
|
-
if (!match) continue
|
|
209
|
-
const depth = match[1].length as 2 | 3
|
|
210
|
-
const text = match[2].trim()
|
|
211
|
-
// Slugged even when skipped, so later duplicates keep the renderer's ids.
|
|
212
|
-
const slug = slugger.slug(text)
|
|
213
|
-
if (!ignored) headings.push({ depth, text, slug })
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return headings
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export interface DocsPageLike {
|
|
220
|
-
id: string
|
|
221
|
-
/** Set when the docs are localised; the id without its locale directory. */
|
|
222
|
-
localeId?: string
|
|
223
|
-
body?: string
|
|
224
|
-
data?: Partial<DocsPageData>
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Frontmatter title, then the first H1, then the slug. */
|
|
228
|
-
export function docTitle(page: DocsPageLike): string {
|
|
229
|
-
return (
|
|
230
|
-
page.data?.title ??
|
|
231
|
-
(page.body ? extractTitleFromMarkdown(page.body) : null) ??
|
|
232
|
-
parseDocCollectionId(routedId(page)).title
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** What pairs a page with its translations: the declared key, or the slug. */
|
|
237
|
-
export function docTranslationKey(page: DocsPageLike): string {
|
|
238
|
-
return page.data?.translationKey ?? getCleanSlug(routedId(page))
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** The name navigation uses. */
|
|
242
|
-
export function docLabel(page: DocsPageLike): string {
|
|
243
|
-
return page.data?.label ?? docTitle(page)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ── Search index ────────────────────────────────────────────────────────────
|
|
247
|
-
|
|
248
|
-
export interface DocsSearchEntry {
|
|
249
|
-
title: string
|
|
250
|
-
slug: string
|
|
251
|
-
chapter: string | null
|
|
252
|
-
keywords: string[]
|
|
253
|
-
body: string
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Strip markdown (and inline HTML) syntax to plain text for search indexing. */
|
|
257
|
-
export function stripMarkdown(text: string): string {
|
|
258
|
-
return text
|
|
259
|
-
.replace(/^#{1,6}\s+/gm, '')
|
|
260
|
-
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
261
|
-
.replace(/\*(.+?)\*/g, '$1')
|
|
262
|
-
.replace(/`{3,}[\s\S]*?`{3,}/g, '')
|
|
263
|
-
.replace(/(?<!`)<[^>]+>(?!`)/g, '')
|
|
264
|
-
.replace(/`(.+?)`/g, '$1')
|
|
265
|
-
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
|
|
266
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
267
|
-
.replace(/^>\s?/gm, '')
|
|
268
|
-
.replace(/^[-*+]\s+/gm, '')
|
|
269
|
-
.replace(/^\d+\.\s+/gm, '')
|
|
270
|
-
.replace(/^:::.*$/gm, '')
|
|
271
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
272
|
-
.trim()
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Excerpt for <meta description>, when the page has no explicit one. */
|
|
276
|
-
export function extractDescription(body: string, title: string, maxLength: number = 155): string | null {
|
|
277
|
-
const stripped = stripMarkdown(body)
|
|
278
|
-
const paragraphs = stripped
|
|
279
|
-
.split(/\n{2,}/)
|
|
280
|
-
.map((p) => p.replace(/\s+/g, ' ').trim())
|
|
281
|
-
.filter(Boolean)
|
|
282
|
-
|
|
283
|
-
const firstParagraph = paragraphs.find((p) => p.toLowerCase() !== title.trim().toLowerCase())
|
|
284
|
-
if (!firstParagraph) return null
|
|
285
|
-
|
|
286
|
-
if (firstParagraph.length <= maxLength) return firstParagraph
|
|
287
|
-
const truncated = firstParagraph.slice(0, maxLength)
|
|
288
|
-
const lastSpace = truncated.lastIndexOf(' ')
|
|
289
|
-
return `${truncated.slice(0, lastSpace > 0 ? lastSpace : maxLength)}…`
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function buildSearchIndex(sortedPages: DocsPageLike[]): DocsSearchEntry[] {
|
|
293
|
-
return sortedPages.map((page, index) => ({
|
|
294
|
-
title: docTitle(page),
|
|
295
|
-
slug: getCanonicalDocSlug(page, index),
|
|
296
|
-
chapter: formatChapterTitle(parseDocCollectionId(routedId(page)).chapter),
|
|
297
|
-
keywords: page.data?.keywords ?? [],
|
|
298
|
-
body: stripMarkdown(page.body || ''),
|
|
299
|
-
}))
|
|
300
|
-
}
|
package/src/utils/endpoints.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import config from 'virtual:jaad/config'
|
|
2
|
-
import { getSortedDocsPages } from '../collection.ts'
|
|
3
|
-
import { docsUrls } from '../docs-config.ts'
|
|
4
|
-
import { docsMarkdownHref } from '../urls.ts'
|
|
5
|
-
import {
|
|
6
|
-
buildSearchIndex,
|
|
7
|
-
docTitle,
|
|
8
|
-
extractDescription,
|
|
9
|
-
formatChapterTitle,
|
|
10
|
-
getCleanSlug,
|
|
11
|
-
parseDocCollectionId,
|
|
12
|
-
routedId,
|
|
13
|
-
} from './docs.ts'
|
|
14
|
-
|
|
15
|
-
export async function searchIndexResponse(locale?: string): Promise<Response> {
|
|
16
|
-
const index = buildSearchIndex(await getSortedDocsPages(locale))
|
|
17
|
-
|
|
18
|
-
return new Response(JSON.stringify(index), {
|
|
19
|
-
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface Row {
|
|
24
|
-
title: string
|
|
25
|
-
slug: string
|
|
26
|
-
description: string | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Plain-text index of every page, per the llms.txt convention. */
|
|
30
|
-
export async function llmsResponse(site: URL | undefined, locale?: string): Promise<Response> {
|
|
31
|
-
const sortedPages = await getSortedDocsPages(locale)
|
|
32
|
-
|
|
33
|
-
const markdownHref = (slug: string): string => {
|
|
34
|
-
const href = docsMarkdownHref(slug, docsUrls(locale))
|
|
35
|
-
return site ? new URL(href, site.origin).href : href
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const standalone: Row[] = []
|
|
39
|
-
const chapters = new Map<string, Row[]>()
|
|
40
|
-
|
|
41
|
-
for (const page of sortedPages) {
|
|
42
|
-
const id = routedId(page)
|
|
43
|
-
const { chapter } = parseDocCollectionId(id)
|
|
44
|
-
const title = docTitle(page)
|
|
45
|
-
const row: Row = {
|
|
46
|
-
title,
|
|
47
|
-
slug: getCleanSlug(id),
|
|
48
|
-
description: page.data.description ?? extractDescription(page.body || '', title),
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!chapter) {
|
|
52
|
-
standalone.push(row)
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const rows = chapters.get(chapter) ?? []
|
|
57
|
-
if (rows.length === 0) chapters.set(chapter, rows)
|
|
58
|
-
rows.push(row)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const toLine = (row: Row): string =>
|
|
62
|
-
`- [${row.title}](${markdownHref(row.slug)})${row.description ? `: ${row.description}` : ''}`
|
|
63
|
-
|
|
64
|
-
const sections: string[] = []
|
|
65
|
-
if (standalone.length > 0) {
|
|
66
|
-
sections.push(standalone.map(toLine).join('\n'))
|
|
67
|
-
}
|
|
68
|
-
for (const [chapter, rows] of chapters) {
|
|
69
|
-
const heading = formatChapterTitle(chapter) ?? chapter
|
|
70
|
-
sections.push(`## ${heading}\n\n${rows.map(toLine).join('\n')}`)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const content = [`# ${config.title}`, ...(config.description ? [`> ${config.description}`] : []), ...sections].join(
|
|
74
|
-
'\n\n',
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
return new Response(content + '\n', {
|
|
78
|
-
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
79
|
-
})
|
|
80
|
-
}
|
package/src/utils/search.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
export interface SearchItem {
|
|
2
|
-
title: string
|
|
3
|
-
slug: string
|
|
4
|
-
chapter: string | null
|
|
5
|
-
keywords?: string[]
|
|
6
|
-
body: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const MAX_RESULTS = 10
|
|
10
|
-
const EMPTY_QUERY_RESULTS = 8
|
|
11
|
-
|
|
12
|
-
interface Folded {
|
|
13
|
-
title: string
|
|
14
|
-
chapter: string
|
|
15
|
-
keywords: string
|
|
16
|
-
body: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Lowercased once per item, not once per keystroke. */
|
|
20
|
-
const folded = new WeakMap<SearchItem, Folded>()
|
|
21
|
-
|
|
22
|
-
function fold(item: SearchItem): Folded {
|
|
23
|
-
let f = folded.get(item)
|
|
24
|
-
if (!f) {
|
|
25
|
-
f = {
|
|
26
|
-
title: item.title.toLowerCase(),
|
|
27
|
-
chapter: item.chapter?.toLowerCase() ?? '',
|
|
28
|
-
keywords: (item.keywords ?? []).join(' ').toLowerCase(),
|
|
29
|
-
body: item.body.toLowerCase(),
|
|
30
|
-
}
|
|
31
|
-
folded.set(item, f)
|
|
32
|
-
}
|
|
33
|
-
return f
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Rank by where the query appears: title, chapter, keywords, then body. */
|
|
37
|
-
export function scoreItems(index: SearchItem[], query: string): SearchItem[] {
|
|
38
|
-
const q = query.trim().toLowerCase()
|
|
39
|
-
if (!q) return index.slice(0, EMPTY_QUERY_RESULTS)
|
|
40
|
-
|
|
41
|
-
return index
|
|
42
|
-
.map((item) => {
|
|
43
|
-
const f = fold(item)
|
|
44
|
-
let score = 0
|
|
45
|
-
if (f.title.includes(q)) score += 10
|
|
46
|
-
if (f.title.startsWith(q)) score += 5
|
|
47
|
-
if (f.chapter.includes(q)) score += 3
|
|
48
|
-
if (f.keywords.includes(q)) score += 2
|
|
49
|
-
if (f.body.includes(q)) score += 1
|
|
50
|
-
return { item, score }
|
|
51
|
-
})
|
|
52
|
-
.filter((s) => s.score > 0)
|
|
53
|
-
.toSorted((a, b) => b.score - a.score)
|
|
54
|
-
.slice(0, MAX_RESULTS)
|
|
55
|
-
.map((s) => s.item)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** A window of body text around the first match, ellipsised at both ends. */
|
|
59
|
-
export function getSnippet(body: string, query: string): string {
|
|
60
|
-
const q = query.trim()
|
|
61
|
-
if (!q) return ''
|
|
62
|
-
const idx = body.toLowerCase().indexOf(q.toLowerCase())
|
|
63
|
-
if (idx === -1) return ''
|
|
64
|
-
|
|
65
|
-
const start = Math.max(0, idx - 40)
|
|
66
|
-
const end = Math.min(body.length, idx + q.length + 80)
|
|
67
|
-
return (start > 0 ? '...' : '') + body.slice(start, end) + (end < body.length ? '...' : '')
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Split around every case-insensitive occurrence, marking the matches. */
|
|
71
|
-
export function splitOnMatches(text: string, query: string): { text: string; match: boolean }[] {
|
|
72
|
-
const q = query.trim()
|
|
73
|
-
if (!q) return [{ text, match: false }]
|
|
74
|
-
|
|
75
|
-
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
76
|
-
return text
|
|
77
|
-
.split(new RegExp(`(${escaped})`, 'gi'))
|
|
78
|
-
.filter((part) => part !== '')
|
|
79
|
-
.map((part) => ({
|
|
80
|
-
text: part,
|
|
81
|
-
match: part.toLowerCase() === q.toLowerCase(),
|
|
82
|
-
}))
|
|
83
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|