@chassis-ui/docs 0.5.1 → 0.6.0-next.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/CHANGELOG.md +256 -0
- package/README.md +227 -71
- package/UPGRADING.md +243 -0
- package/index.ts +7 -1
- package/package.json +31 -8
- package/src/components/DocsSidebar.astro +9 -9
- package/src/components/shortcodes/Callout.astro +4 -4
- package/src/components/shortcodes/Code.astro +3 -3
- package/src/components/shortcodes/JsDocs.astro +6 -7
- package/src/components/shortcodes/ScssDocs.astro +7 -9
- package/src/components/shortcodes/ScssDocsSimple.astro +6 -7
- package/src/integration.ts +271 -0
- package/src/js/clipboard.ts +65 -0
- package/src/layouts/BaseLayout.astro +7 -9
- package/src/layouts/DocsLayout.astro +11 -13
- package/src/layouts/RedirectLayout.astro +1 -1
- package/src/layouts/head/Analytics.astro +6 -4
- package/src/layouts/head/Head.astro +2 -2
- package/src/layouts/head/Social.astro +5 -6
- package/src/layouts/header/Navigation.astro +28 -19
- package/src/libs/config.ts +138 -0
- package/src/libs/docs-path.ts +12 -0
- package/src/libs/highlight.ts +4 -44
- package/src/libs/paths.ts +160 -0
- package/src/libs/remark.ts +184 -0
- package/src/libs/schema.ts +211 -0
- package/src/libs/shortcodes.ts +86 -71
- package/src/libs/snippet.ts +21 -0
- package/src/libs/source.ts +17 -40
- package/src/libs/toc.ts +3 -1
- package/src/scss/_vars.scss +4 -1
- package/src/site.ts +150 -0
- package/virtual.d.ts +25 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
|
|
3
|
+
// Sites extend the schemas below. They must use this `z`, so that the site and the package
|
|
4
|
+
// share one copy of Zod.
|
|
5
|
+
export { z }
|
|
6
|
+
|
|
7
|
+
export const zVersionMajorMinor = z.string().regex(/^\d+\.\d+$/)
|
|
8
|
+
|
|
9
|
+
// https://ihateregex.io/expr/semver/
|
|
10
|
+
const unboundSemverRegex =
|
|
11
|
+
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/
|
|
12
|
+
|
|
13
|
+
export const zVersionSemver = z.string().regex(new RegExp(`^${unboundSemverRegex.source}$`))
|
|
14
|
+
export const zPrefixedVersionSemver = z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(new RegExp(`^v${unboundSemverRegex.source}$`))
|
|
17
|
+
|
|
18
|
+
export const zLanguageCode = z.string().regex(/^[a-z]{2}(?:-[a-zA-Z]{2})?$/)
|
|
19
|
+
|
|
20
|
+
const zHeadingRange = z.object({
|
|
21
|
+
min: z.number().int().min(1).max(6),
|
|
22
|
+
max: z.number().int().min(1).max(6)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The keys of `config.yml` that the package reads.
|
|
27
|
+
*
|
|
28
|
+
* The schema is strict: a key that is not in it fails the build. A site with keys of its own
|
|
29
|
+
* extends the schema and passes the result to the integration.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { configSchema, z } from '@chassis-ui/docs/schema'
|
|
34
|
+
*
|
|
35
|
+
* export const siteConfigSchema = configSchema.extend({
|
|
36
|
+
* blog: z.object({ pageSize: z.number() })
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const configSchema = z.strictObject({
|
|
41
|
+
/** Google Analytics is loaded in production builds when `googleId` is set. */
|
|
42
|
+
analytics: z.strictObject({ googleId: z.string().optional() }).default({}),
|
|
43
|
+
/** Heading levels that get an anchor link. */
|
|
44
|
+
anchors: zHeadingRange.default({ min: 2, max: 5 }),
|
|
45
|
+
authors: z.string(),
|
|
46
|
+
/** Canonical URL of the site, e.g. `"https://chassis-ui.com/css"`. */
|
|
47
|
+
baseURL: z.url(),
|
|
48
|
+
/** Version of the release that the site documents. Links to source files use its tag. */
|
|
49
|
+
currentVersion: zVersionSemver,
|
|
50
|
+
description: z.string(),
|
|
51
|
+
/** URL path of the docs pages, e.g. `"/css/docs"`. */
|
|
52
|
+
docsPath: z.string().startsWith('/'),
|
|
53
|
+
/** Figma Community handle, without the `@`. The header links to it when set. */
|
|
54
|
+
figmaHandle: z.string().optional(),
|
|
55
|
+
/** GitHub organisation or user name, not a URL. The header links to it when set. */
|
|
56
|
+
githubOrg: z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(/^[\w.-]+$/, 'Expected a GitHub organisation name, not a URL')
|
|
59
|
+
.optional(),
|
|
60
|
+
/** Repository URL, e.g. `"https://github.com/chassis-ui/css"`. */
|
|
61
|
+
repo: z.url(),
|
|
62
|
+
/** The branch that "View on GitHub" links to. */
|
|
63
|
+
siteBranch: z.string().default('main'),
|
|
64
|
+
/** The site's root from the root of the repository, e.g. `"packages/site"`. */
|
|
65
|
+
sitePath: z.string().optional(),
|
|
66
|
+
/**
|
|
67
|
+
* Directory that the `file` props of `<ScssDocs>`, `<ScssDocsSimple>`, `<JsDocs>` and
|
|
68
|
+
* `<Code>` are relative to, itself relative to the site's root, e.g. `"../css"`.
|
|
69
|
+
*/
|
|
70
|
+
sourceDir: z.string().default('.'),
|
|
71
|
+
/** The same directory from the root of the repository, e.g. `"packages/css"`. */
|
|
72
|
+
sourcePath: z.string().optional(),
|
|
73
|
+
subtitle: z.string(),
|
|
74
|
+
title: z.string(),
|
|
75
|
+
/** Heading levels that the table of contents lists. */
|
|
76
|
+
toc: zHeadingRange.default({ min: 2, max: 6 }),
|
|
77
|
+
/** X handle, without the `@`. Used in the social meta tags when set. */
|
|
78
|
+
xUsername: z.string().optional()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/** The parsed `config.yml`, limited to the keys that the package reads. */
|
|
82
|
+
export type ChassisConfig = z.infer<typeof configSchema>
|
|
83
|
+
|
|
84
|
+
/** A schema that a site passes to the integration: `configSchema` or an extension of it. */
|
|
85
|
+
export type ChassisConfigSchema = z.ZodType<ChassisConfig>
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Keys that were renamed in 0.6.0. Loading a `config.yml` that still has one of them fails
|
|
89
|
+
* with a message that names the new key.
|
|
90
|
+
*/
|
|
91
|
+
export const renamedConfigKeys: Record<string, string> = {
|
|
92
|
+
'analytics.google_id': 'analytics.googleId',
|
|
93
|
+
current_version: 'currentVersion',
|
|
94
|
+
figma_handle: 'figmaHandle',
|
|
95
|
+
github_org: 'githubOrg',
|
|
96
|
+
x_username: 'xUsername'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Keys that were removed in 0.6.0, with the reason. */
|
|
100
|
+
export const removedConfigKeys: Record<string, string> = {
|
|
101
|
+
docsDir: 'The site root is the Astro root. Delete the key.'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A key that was renamed in 0.6.0. The schemas below are not strict, so the old key would
|
|
105
|
+
// be dropped silently. This makes it fail, with a message that names the new key.
|
|
106
|
+
function renamedTo(newKey: string) {
|
|
107
|
+
return z.never({ error: `This key was renamed to \`${newKey}\`.` }).optional()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sidebarMetaSchema = z.object({ added: z.string() }).array().optional()
|
|
111
|
+
|
|
112
|
+
/** The entries of `data/sidebar.yml`. */
|
|
113
|
+
export const sidebarSchema = z
|
|
114
|
+
.object({
|
|
115
|
+
title: z.string(),
|
|
116
|
+
section: z.string().optional(),
|
|
117
|
+
icon: z.string().optional(),
|
|
118
|
+
iconColor: z.string().optional(),
|
|
119
|
+
icon_color: renamedTo('iconColor'),
|
|
120
|
+
pages: z
|
|
121
|
+
.object({
|
|
122
|
+
title: z.string().optional(),
|
|
123
|
+
href: z.string().optional(),
|
|
124
|
+
group: z.string().optional(),
|
|
125
|
+
meta: sidebarMetaSchema,
|
|
126
|
+
pages: z
|
|
127
|
+
.object({
|
|
128
|
+
title: z.string(),
|
|
129
|
+
meta: sidebarMetaSchema
|
|
130
|
+
})
|
|
131
|
+
.array()
|
|
132
|
+
.optional()
|
|
133
|
+
})
|
|
134
|
+
.array()
|
|
135
|
+
.optional()
|
|
136
|
+
})
|
|
137
|
+
.array()
|
|
138
|
+
|
|
139
|
+
export type Sidebar = z.infer<typeof sidebarSchema>
|
|
140
|
+
export type SidebarGroup = Sidebar[number]
|
|
141
|
+
export type SidebarItem = NonNullable<SidebarGroup['pages']>[number]
|
|
142
|
+
export type SidebarSubItem = NonNullable<SidebarItem['pages']>[number]
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Frontmatter of a page in the `docs` collection.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* // src/content.config.ts
|
|
150
|
+
* import { defineCollection } from 'astro:content'
|
|
151
|
+
* import { glob } from 'astro/loaders'
|
|
152
|
+
* import { calloutsSchema, docsSchema } from '@chassis-ui/docs/schema'
|
|
153
|
+
*
|
|
154
|
+
* export const collections = {
|
|
155
|
+
* docs: defineCollection({
|
|
156
|
+
* loader: glob({ pattern: '**\/*.{md,mdx}', base: './content/docs' }),
|
|
157
|
+
* schema: docsSchema
|
|
158
|
+
* }),
|
|
159
|
+
* callouts: defineCollection({
|
|
160
|
+
* loader: glob({ pattern: '**\/*.md', base: './content/callouts' }),
|
|
161
|
+
* schema: calloutsSchema
|
|
162
|
+
* })
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export const docsSchema = z
|
|
167
|
+
.object({
|
|
168
|
+
added: z
|
|
169
|
+
.object({
|
|
170
|
+
showBadge: z.boolean().optional(),
|
|
171
|
+
show_badge: renamedTo('showBadge'),
|
|
172
|
+
version: z.string()
|
|
173
|
+
})
|
|
174
|
+
.optional(),
|
|
175
|
+
aliases: z.string().or(z.string().array()).optional(),
|
|
176
|
+
description: z.string(),
|
|
177
|
+
direction: z.literal('rtl').optional(),
|
|
178
|
+
extraJs: z
|
|
179
|
+
.object({
|
|
180
|
+
async: z.boolean().optional(),
|
|
181
|
+
src: z.string()
|
|
182
|
+
})
|
|
183
|
+
.array()
|
|
184
|
+
.optional(),
|
|
185
|
+
extra_js: renamedTo('extraJs'),
|
|
186
|
+
sections: z
|
|
187
|
+
.object({
|
|
188
|
+
description: z.string(),
|
|
189
|
+
title: z.string(),
|
|
190
|
+
slug: z.string().optional()
|
|
191
|
+
})
|
|
192
|
+
.array()
|
|
193
|
+
.optional(),
|
|
194
|
+
thumbnail: z.string().optional(),
|
|
195
|
+
title: z.string(),
|
|
196
|
+
toc: z.boolean().optional()
|
|
197
|
+
})
|
|
198
|
+
.partial()
|
|
199
|
+
|
|
200
|
+
export type DocsFrontmatter = z.infer<typeof docsSchema>
|
|
201
|
+
|
|
202
|
+
/** Frontmatter of an entry in the `callouts` collection. */
|
|
203
|
+
export const calloutsSchema = z.object({})
|
|
204
|
+
|
|
205
|
+
/** An entry of the `docs` collection, as far as the package reads it. */
|
|
206
|
+
export interface DocsPage {
|
|
207
|
+
id: string
|
|
208
|
+
data: DocsFrontmatter
|
|
209
|
+
/** Path of the source file from the site's root. */
|
|
210
|
+
filePath?: string
|
|
211
|
+
}
|
package/src/libs/shortcodes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import
|
|
3
|
+
import { resolvePackageFilePath } from './paths'
|
|
4
4
|
|
|
5
5
|
interface MdxjsEsmNode {
|
|
6
6
|
type: 'mdxjsEsm'
|
|
@@ -8,9 +8,13 @@ interface MdxjsEsmNode {
|
|
|
8
8
|
data: { estree: object }
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function getComponentName(filePath: string): string {
|
|
12
|
+
return path.parse(filePath).name.replaceAll(/[^\w\d]/g, '')
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
function buildImportsNode(imports: string[]): MdxjsEsmNode {
|
|
12
16
|
const body = imports.map((filePath) => {
|
|
13
|
-
const name =
|
|
17
|
+
const name = getComponentName(filePath)
|
|
14
18
|
return {
|
|
15
19
|
type: 'ImportDeclaration',
|
|
16
20
|
specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name } }],
|
|
@@ -26,89 +30,100 @@ function buildImportsNode(imports: string[]): MdxjsEsmNode {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export interface ChassisAutoImportOptions {
|
|
29
|
-
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The site's root.
|
|
35
|
+
* @default process.cwd()
|
|
36
|
+
*/
|
|
37
|
+
root?: string
|
|
38
|
+
/**
|
|
39
|
+
* The site's own shortcodes, relative to `root`. A component here replaces the component
|
|
40
|
+
* of the package that has the same name.
|
|
41
|
+
* @default 'src/components/shortcodes'
|
|
42
|
+
*/
|
|
43
|
+
dir?: string
|
|
44
|
+
/** Names of the only shortcodes of the package to import, e.g. `['Code', 'Callout']`. */
|
|
45
|
+
include?: string[]
|
|
46
|
+
/** Names of shortcodes of the package to leave out, e.g. `['Icon']`. */
|
|
47
|
+
exclude?: string[]
|
|
48
|
+
/**
|
|
49
|
+
* Absolute path of the package's shortcodes. Found from the package's own location when
|
|
50
|
+
* omitted.
|
|
51
|
+
*/
|
|
52
|
+
packageDir?: string
|
|
35
53
|
}
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
export interface ChassisAutoImport {
|
|
56
|
+
/** Import specifiers of the components, in import order. */
|
|
57
|
+
imports: string[]
|
|
58
|
+
/** Source of a declaration file that makes the components global in MDX files. */
|
|
59
|
+
typeDefinitions: string
|
|
60
|
+
/** Factory for the remark plugin that injects the imports into every `.mdx` file. */
|
|
61
|
+
plugin: () => ReturnType<typeof createAutoImportPlugin>
|
|
62
|
+
}
|
|
42
63
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for (const dir of dirs) {
|
|
47
|
-
if (!fs.existsSync(dir)) continue
|
|
48
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
-
if (!entry.isFile()) continue
|
|
50
|
-
components.push(`${dir}/${entry.name}`)
|
|
51
|
-
if (entry.name.endsWith('.astro')) {
|
|
52
|
-
const importPath = dir.includes('@chassis-ui/docs')
|
|
53
|
-
? `@chassis-ui/docs/shortcodes`
|
|
54
|
-
: `@shortcodes`
|
|
55
|
-
definitions.push(
|
|
56
|
-
`export const ${entry.name.replace('.astro', '')}: typeof import('${importPath}/${entry.name}').default`
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
64
|
+
function listFiles(directory: string): string[] {
|
|
65
|
+
if (!fs.existsSync(directory)) return []
|
|
61
66
|
|
|
62
|
-
return
|
|
67
|
+
return fs
|
|
68
|
+
.readdirSync(directory, { withFileTypes: true })
|
|
69
|
+
.filter((entry) => entry.isFile() && !entry.name.startsWith('.'))
|
|
70
|
+
.map((entry) => entry.name)
|
|
71
|
+
.sort()
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* Replaces `astro-auto-import`, which triggers a deprecation warning in Astro 7.
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* ```ts
|
|
73
|
-
* // astro.ts
|
|
74
|
-
* const { integration, plugin } = chassisAutoImport({ docsPath: getDocsFsPath(), modulesPath: process.cwd() })
|
|
75
|
-
*
|
|
76
|
-
* // astro.config.ts
|
|
77
|
-
* markdown: getDocsMarkdownConfig({ remarkPlugins: [plugin()] })
|
|
78
|
-
* integrations: [integration()]
|
|
79
|
-
* ```
|
|
75
|
+
* Finds the shortcodes of the package and of the site, and returns what is needed to import
|
|
76
|
+
* them into every MDX file. The `chassisDocs()` integration calls it. A site calls it only
|
|
77
|
+
* to build its own markdown configuration.
|
|
80
78
|
*/
|
|
81
|
-
export function chassisAutoImport(options: ChassisAutoImportOptions): {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
export function chassisAutoImport(options: ChassisAutoImportOptions = {}): ChassisAutoImport {
|
|
80
|
+
const {
|
|
81
|
+
root = process.cwd(),
|
|
82
|
+
dir = 'src/components/shortcodes',
|
|
83
|
+
include,
|
|
84
|
+
exclude = [],
|
|
85
|
+
packageDir = resolvePackageFilePath('shortcodes')
|
|
86
|
+
} = options
|
|
87
|
+
|
|
88
|
+
const siteDir = path.resolve(root, dir)
|
|
89
|
+
const siteFiles = listFiles(siteDir)
|
|
90
|
+
const siteNames = new Set(siteFiles.map(getComponentName))
|
|
91
|
+
|
|
92
|
+
const packageFiles = listFiles(packageDir).filter((file) => {
|
|
93
|
+
const name = getComponentName(file)
|
|
94
|
+
|
|
95
|
+
if (siteNames.has(name) || exclude.includes(name)) return false
|
|
96
|
+
|
|
97
|
+
return include ? include.includes(name) : true
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// The package's components are imported by specifier, not by path. The path of an
|
|
101
|
+
// installed package is outside the site, where the dev server refuses to serve files.
|
|
102
|
+
const components = [
|
|
103
|
+
...packageFiles.map((file) => ({ file, source: `@chassis-ui/docs/shortcodes/${file}` })),
|
|
104
|
+
...siteFiles.map((file) => ({ file, source: path.join(siteDir, file) }))
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
const definitions = components
|
|
108
|
+
.filter(({ file }) => file.endsWith('.astro'))
|
|
109
|
+
.map(
|
|
110
|
+
({ file, source }) =>
|
|
111
|
+
`export const ${getComponentName(file)}: typeof import('${source}').default`
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const imports = components.map(({ source }) => source)
|
|
89
115
|
|
|
90
116
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fs.writeFileSync(
|
|
96
|
-
typeDefinitionsPath,
|
|
97
|
-
`/**
|
|
98
|
-
* DO NOT EDIT THIS FILE MANUALLY.
|
|
99
|
-
*
|
|
100
|
-
* This file is automatically generated by the Chassis Astro Integration.
|
|
101
|
-
* It contains the type definitions for the components that are auto imported in all pages.
|
|
117
|
+
imports,
|
|
118
|
+
typeDefinitions: `/**
|
|
119
|
+
* Generated by the integration of @chassis-ui/docs.
|
|
120
|
+
* The components that are imported into every MDX file.
|
|
102
121
|
*/
|
|
103
122
|
export declare global {
|
|
104
123
|
${definitions.join('\n ')}
|
|
105
124
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}),
|
|
111
|
-
plugin: () => createAutoImportPlugin(components)
|
|
125
|
+
`,
|
|
126
|
+
plugin: () => createAutoImportPlugin(imports)
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
129
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the text between `// <kind>-docs-start <name>` and the first
|
|
3
|
+
* `// <kind>-docs-end <name>` after it, or `undefined` when the file has no such part.
|
|
4
|
+
*
|
|
5
|
+
* The end marker has to name the part exactly: the part `make-col` ends at
|
|
6
|
+
* `// scss-docs-end make-col`, not at `// scss-docs-end make-col-auto`.
|
|
7
|
+
*/
|
|
8
|
+
export function extractDocsSnippet(
|
|
9
|
+
content: string,
|
|
10
|
+
kind: 'js' | 'scss',
|
|
11
|
+
name: string
|
|
12
|
+
): string | undefined {
|
|
13
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
14
|
+
const matches = content.match(
|
|
15
|
+
new RegExp(
|
|
16
|
+
`// ${kind}-docs-start ${escapedName}\\n([\\s\\S]*?)// ${kind}-docs-end ${escapedName}(?![\\w-])`
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return matches?.[1] || undefined
|
|
21
|
+
}
|
package/src/libs/source.ts
CHANGED
|
@@ -1,34 +1,11 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import {
|
|
2
|
+
import type { ChassisConfig } from './schema'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
interface SourceConfig {
|
|
11
|
-
/** Repository URL (e.g. `"https://github.com/chassis-ui/css"`). */
|
|
12
|
-
repo: string
|
|
13
|
-
/** Version of the release that links to source files name. */
|
|
14
|
-
current_version: string
|
|
15
|
-
/**
|
|
16
|
-
* Directory the `file` props of `<ScssDocs>`, `<ScssDocsSimple>`, `<JsDocs>` and `<Code>` are
|
|
17
|
-
* relative to, itself relative to the working directory of the build (e.g. `"../css"` for a
|
|
18
|
-
* site in `packages/site`).
|
|
19
|
-
*/
|
|
20
|
-
sourceDir?: string
|
|
21
|
-
/** The same directory from the root of the repository (e.g. `"packages/css"`). */
|
|
22
|
-
sourcePath?: string
|
|
23
|
-
/** The site's root from the root of the repository (e.g. `"packages/site"`). */
|
|
24
|
-
sitePath?: string
|
|
25
|
-
/** The branch that "View on GitHub" links to. Defaults to `"main"`. */
|
|
26
|
-
siteBranch?: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getSourceConfig(): SourceConfig {
|
|
30
|
-
return getConfig() as SourceConfig
|
|
31
|
-
}
|
|
4
|
+
/** The keys of `config.yml` that say where files live, on disk and on GitHub. */
|
|
5
|
+
export type SourceConfig = Pick<
|
|
6
|
+
ChassisConfig,
|
|
7
|
+
'repo' | 'currentVersion' | 'siteBranch' | 'sitePath' | 'sourcePath'
|
|
8
|
+
>
|
|
32
9
|
|
|
33
10
|
/** Joins URL segments, skipping empty ones, and resolves any `..` in them. */
|
|
34
11
|
function toUrl(segments: (string | undefined)[]): string {
|
|
@@ -37,16 +14,18 @@ function toUrl(segments: (string | undefined)[]): string {
|
|
|
37
14
|
return new URL(url).href
|
|
38
15
|
}
|
|
39
16
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Absolute path of a file or directory that a `file` prop names.
|
|
19
|
+
*
|
|
20
|
+
* @param sourceDir Absolute path of the directory that `file` props are relative to.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveSourceFsPath(sourceDir: string, file = ''): string {
|
|
23
|
+
return path.resolve(sourceDir, file)
|
|
43
24
|
}
|
|
44
25
|
|
|
45
26
|
/** URL of a source file on GitHub, in the tag of the current version. */
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return toUrl([repo, 'blob', `v${version}`, sourcePath, file])
|
|
27
|
+
export function resolveSourceUrl(config: SourceConfig, file: string): string {
|
|
28
|
+
return toUrl([config.repo, 'blob', `v${config.currentVersion}`, config.sourcePath, file])
|
|
50
29
|
}
|
|
51
30
|
|
|
52
31
|
/**
|
|
@@ -55,8 +34,6 @@ export function getSourceUrl(file: string): string {
|
|
|
55
34
|
* @param filePath Path of the file from the site's root, as Astro stores it in a content
|
|
56
35
|
* entry's `filePath` (e.g. `"content/docs/getting-started/introduction.mdx"`).
|
|
57
36
|
*/
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return toUrl([repo, 'blob', siteBranch, sitePath, filePath])
|
|
37
|
+
export function resolveSiteFileUrl(config: SourceConfig, filePath: string): string {
|
|
38
|
+
return toUrl([config.repo, 'blob', config.siteBranch, config.sitePath, filePath])
|
|
62
39
|
}
|
package/src/libs/toc.ts
CHANGED
|
@@ -37,7 +37,9 @@ export function generateToc(allHeadings: MarkdownHeading[], config: TocConfig):
|
|
|
37
37
|
|
|
38
38
|
const previousEntry = toc[toc.length - 1]
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// A heading at the depth of the previous top-level entry, or shallower, starts a new
|
|
41
|
+
// top-level entry. A page whose first heading is an `<h3>` has later `<h2>`s here.
|
|
42
|
+
if (heading.depth <= previousEntry.depth) {
|
|
41
43
|
toc.push({ ...heading, children: [] })
|
|
42
44
|
continue
|
|
43
45
|
}
|
package/src/scss/_vars.scss
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The Sass variables and custom properties of the docs.
|
|
2
|
+
//
|
|
3
|
+
// This partial is public: a site may `@use "@chassis-ui/docs/scss/vars"` in its own
|
|
4
|
+
// stylesheets. Removing or renaming a variable is a breaking change.
|
|
2
5
|
@use "@chassis-ui/css/scss/functions" as *;
|
|
3
6
|
@use "@chassis-ui/css/scss/mixins" as *;
|
|
4
7
|
|
package/src/site.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// What pages and components read from the site. It is backed by the modules that the
|
|
2
|
+
// integration provides, so it works only in code that Astro renders: not in
|
|
3
|
+
// `astro.config.ts`, and not in the browser.
|
|
4
|
+
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import type { z } from 'astro/zod'
|
|
7
|
+
import config from 'virtual:chassis-docs/config'
|
|
8
|
+
import sidebar from 'virtual:chassis-docs/sidebar'
|
|
9
|
+
import * as paths from 'virtual:chassis-docs/paths'
|
|
10
|
+
import { getCollection, getEntry, type render } from 'astro:content'
|
|
11
|
+
import { loadData } from './libs/config'
|
|
12
|
+
import { joinDocsPath } from './libs/docs-path'
|
|
13
|
+
import { getNodeModulesFsPaths, resolvePackageFilePath } from './libs/paths'
|
|
14
|
+
import { replaceConfigInText, replaceDocsrefInText } from './libs/remark'
|
|
15
|
+
import type { ChassisConfig, DocsPage, Sidebar } from './libs/schema'
|
|
16
|
+
import { resolveSiteFileUrl, resolveSourceFsPath, resolveSourceUrl } from './libs/source'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The parsed `config.yml`.
|
|
20
|
+
*
|
|
21
|
+
* A site with keys of its own names its config type: `getConfig<SiteConfig>()`, where
|
|
22
|
+
* `SiteConfig` is inferred from the schema that the site passes to the integration.
|
|
23
|
+
*/
|
|
24
|
+
export function getConfig<TConfig extends ChassisConfig = ChassisConfig>(): TConfig {
|
|
25
|
+
return config as TConfig
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The parsed `data/sidebar.yml`. Empty when the site has no sidebar file. */
|
|
29
|
+
export function getSidebar(): Sidebar {
|
|
30
|
+
return sidebar
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** URL path of a docs page, e.g. `getDocsPath('/components/button/')`. */
|
|
34
|
+
export function getDocsPath(inputPath: string): string {
|
|
35
|
+
return joinDocsPath(config.docsPath, inputPath)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Absolute path of the site's root: the directory that holds `astro.config.ts`. */
|
|
39
|
+
export function getSiteRoot(): string {
|
|
40
|
+
return paths.root
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Absolute path of a file in the site's root. */
|
|
44
|
+
export function getSiteFsPath(file = ''): string {
|
|
45
|
+
return path.resolve(paths.root, file)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Absolute path of a file in the site's `public` directory. */
|
|
49
|
+
export function getPublicFsPath(file = ''): string {
|
|
50
|
+
return path.join(paths.publicDir, file)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Absolute path of a file or directory that a `file` prop names. */
|
|
54
|
+
export function getSourceFsPath(file = ''): string {
|
|
55
|
+
return resolveSourceFsPath(paths.sourceDir, file)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** URL of a source file on GitHub, in the tag of the current version. */
|
|
59
|
+
export function getSourceUrl(file: string): string {
|
|
60
|
+
return resolveSourceUrl(config, file)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** URL of a file of the site on GitHub, on the site's branch. */
|
|
64
|
+
export function getSiteFileUrl(filePath: string): string {
|
|
65
|
+
return resolveSiteFileUrl(config, filePath)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Absolute path of a file of the package, named by its public path, e.g.
|
|
70
|
+
* `getPackageFilePath('js/color-modes.js')`.
|
|
71
|
+
*/
|
|
72
|
+
export function getPackageFilePath(file: string): string {
|
|
73
|
+
return resolvePackageFilePath(file, paths.packageRoot)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The `node_modules` directories that the site resolves packages from, nearest first. */
|
|
77
|
+
export function getNodeModulesPaths(): string[] {
|
|
78
|
+
return getNodeModulesFsPaths(paths.root)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns a `getData(name)` function for the site's own data files. Each key of
|
|
83
|
+
* `definitions` is the name of a YAML file in `dir`, and its value is the schema of the file.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { createDataLoader } from '@chassis-ui/docs/site'
|
|
88
|
+
* import { z } from '@chassis-ui/docs/schema'
|
|
89
|
+
*
|
|
90
|
+
* export const getData = createDataLoader({
|
|
91
|
+
* breakpoints: z.object({ name: z.string() }).array()
|
|
92
|
+
* })
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function createDataLoader<TDefinitions extends Record<string, z.ZodType>>(
|
|
96
|
+
definitions: TDefinitions,
|
|
97
|
+
options: { dir?: string } = {}
|
|
98
|
+
) {
|
|
99
|
+
const cache = new Map<keyof TDefinitions, unknown>()
|
|
100
|
+
|
|
101
|
+
return function getData<TName extends keyof TDefinitions & string>(
|
|
102
|
+
name: TName
|
|
103
|
+
): z.infer<TDefinitions[TName]> {
|
|
104
|
+
if (!cache.has(name)) {
|
|
105
|
+
cache.set(
|
|
106
|
+
name,
|
|
107
|
+
loadData({
|
|
108
|
+
file: path.join(options.dir ?? 'data', `${name}.yml`),
|
|
109
|
+
root: paths.root,
|
|
110
|
+
schema: definitions[name]
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return cache.get(name) as z.infer<TDefinitions[TName]>
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type RenderableEntry = Parameters<typeof render>[0]
|
|
120
|
+
|
|
121
|
+
// The package cannot know the collections of the site, so their names are not type-checked.
|
|
122
|
+
const getSiteCollection = getCollection as unknown as (_name: string) => Promise<DocsPage[]>
|
|
123
|
+
const getSiteEntry = getEntry as unknown as (
|
|
124
|
+
_collection: string,
|
|
125
|
+
_id: string
|
|
126
|
+
) => Promise<RenderableEntry | undefined>
|
|
127
|
+
|
|
128
|
+
let docsPages: Promise<DocsPage[]> | undefined
|
|
129
|
+
|
|
130
|
+
/** The entries of the site's `docs` collection. */
|
|
131
|
+
export function getDocsPages(): Promise<DocsPage[]> {
|
|
132
|
+
docsPages ??= getSiteCollection('docs')
|
|
133
|
+
|
|
134
|
+
return docsPages
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** An entry of the site's `callouts` collection, or `undefined` when there is none. */
|
|
138
|
+
export function getCallout(name: string): Promise<RenderableEntry | undefined> {
|
|
139
|
+
return getSiteEntry('callouts', name)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Replaces each `[[config:key]]` in `text` with the value of that key of `config.yml`. */
|
|
143
|
+
export function resolveConfigRefs(text: string): string {
|
|
144
|
+
return replaceConfigInText(text, config)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Replaces each `[[docsref:/path]]` in `text` with the URL path of that docs page. */
|
|
148
|
+
export function resolveDocsrefs(text: string): string {
|
|
149
|
+
return replaceDocsrefInText(text, { docsPath: config.docsPath })
|
|
150
|
+
}
|