@chassis-ui/docs 0.5.1 → 0.6.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 +286 -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
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Main layout wrapper for all pages with configurable HTML attributes and layout modes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { CollectionEntry } from 'astro:content'
|
|
9
|
-
import { getConfig } from '@libs/config'
|
|
10
8
|
import type { Layout, LayoutOverridesHTMLAttributes } from '../libs/layout'
|
|
9
|
+
import type { DocsFrontmatter } from '../libs/schema'
|
|
10
|
+
import { getConfig } from '../site'
|
|
11
11
|
import Head from './head/Head.astro'
|
|
12
12
|
import Header from './header/Header.astro'
|
|
13
13
|
import Scripts from './footer/Scripts.astro'
|
|
@@ -42,11 +42,11 @@ type Props = {
|
|
|
42
42
|
// The following props are automatically set by Astro (if defined) based on the markdown frontmatter when rendering a
|
|
43
43
|
// markdown page. They can be accessed through the `Astro.props.frontmatter` object but note that they won't be set when
|
|
44
44
|
// not rendering a markdown page.
|
|
45
|
-
type MarkdownProps = { frontmatter?:
|
|
45
|
+
type MarkdownProps = { frontmatter?: DocsFrontmatter }
|
|
46
46
|
|
|
47
|
-
// This type is used to define the shape of the `
|
|
47
|
+
// This type is used to define the shape of the `extraJs` frontmatter property which allows adding extra JavaScript
|
|
48
48
|
// files to a page by passing an array of objects containing the `src` and `async` properties.
|
|
49
|
-
type ExtraJS = NonNullable<
|
|
49
|
+
type ExtraJS = NonNullable<DocsFrontmatter['extraJs']>[number]
|
|
50
50
|
|
|
51
51
|
const { frontmatter, layout, overrides, robots } = Astro.props
|
|
52
52
|
|
|
@@ -96,10 +96,8 @@ const mainProps = overrides?.main ?? {}
|
|
|
96
96
|
</Scripts>
|
|
97
97
|
|
|
98
98
|
{
|
|
99
|
-
frontmatter?.
|
|
100
|
-
frontmatter.
|
|
101
|
-
<script is:inline async={js.async} src={js.src} />
|
|
102
|
-
))
|
|
99
|
+
frontmatter?.extraJs &&
|
|
100
|
+
frontmatter.extraJs.map((js: ExtraJS) => <script is:inline async={js.async} src={js.src} />)
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
{
|
|
@@ -6,13 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { MarkdownHeading } from 'astro'
|
|
9
|
-
import type { CollectionEntry } from 'astro:content'
|
|
10
|
-
import { getConfig } from '@libs/config'
|
|
11
|
-
import { docsPages } from '@libs/content'
|
|
12
|
-
import { getChassisDocsPath } from '@libs/path'
|
|
13
9
|
import type { LayoutOverridesHTMLAttributes } from '../libs/layout'
|
|
14
|
-
import {
|
|
10
|
+
import type { DocsFrontmatter } from '../libs/schema'
|
|
15
11
|
import { getSlug, processMarkdownToHtml } from '../libs/utils'
|
|
12
|
+
import { getConfig, getDocsPages, getDocsPath, getSiteFileUrl } from '../site'
|
|
16
13
|
import BaseLayout from './BaseLayout.astro'
|
|
17
14
|
import DocsSidebar from '../components/DocsSidebar.astro'
|
|
18
15
|
import TableOfContents from '../components/TableOfContents.astro'
|
|
@@ -20,11 +17,11 @@ import Icon from '../components/shortcodes/Icon.astro'
|
|
|
20
17
|
|
|
21
18
|
interface Props {
|
|
22
19
|
/** Frontmatter data from the docs collection */
|
|
23
|
-
frontmatter:
|
|
20
|
+
frontmatter: DocsFrontmatter
|
|
24
21
|
/** Markdown headings for table of contents */
|
|
25
22
|
headings?: MarkdownHeading[]
|
|
26
23
|
/** Document ID from the docs collection */
|
|
27
|
-
id:
|
|
24
|
+
id: string
|
|
28
25
|
/**
|
|
29
26
|
* Path of the page's source file from the site's root. Looked up from the docs collection
|
|
30
27
|
* by `id` when omitted.
|
|
@@ -36,7 +33,8 @@ const { frontmatter, headings, id } = Astro.props
|
|
|
36
33
|
|
|
37
34
|
// "View on GitHub" links to the page's own source file. The old link, which assumed the
|
|
38
35
|
// file sat at `<id>.mdx` in the repository root, is kept only when no file path is known.
|
|
39
|
-
const filePath =
|
|
36
|
+
const filePath =
|
|
37
|
+
Astro.props.filePath ?? (await getDocsPages()).find((page) => page.id === id)?.filePath
|
|
40
38
|
const githubUrl = filePath ? getSiteFileUrl(filePath) : `${getConfig().repo}/blob/${id}.mdx`
|
|
41
39
|
|
|
42
40
|
type DocSection = NonNullable<typeof frontmatter.sections>[number]
|
|
@@ -86,11 +84,11 @@ if (frontmatter.toc) {
|
|
|
86
84
|
<div class="md:d-flex md:flex-row-reverse justify-content-between align-items-start">
|
|
87
85
|
<div class="mb-md md:mb-0 d-flex text-nowrap">
|
|
88
86
|
{
|
|
89
|
-
// This is needed because we want to show the badge if
|
|
87
|
+
// This is needed because we want to show the badge if showBadge isn't present or is set to false
|
|
90
88
|
frontmatter.added &&
|
|
91
|
-
((frontmatter.added.
|
|
92
|
-
frontmatter.added.
|
|
93
|
-
frontmatter.added.
|
|
89
|
+
((frontmatter.added.showBadge !== undefined &&
|
|
90
|
+
frontmatter.added.showBadge === true) ||
|
|
91
|
+
frontmatter.added.showBadge === undefined) && (
|
|
94
92
|
<span class="badge success smooth rounded-md me-xs">
|
|
95
93
|
Added in v{frontmatter.added.version}
|
|
96
94
|
</span>
|
|
@@ -153,7 +151,7 @@ if (frontmatter.toc) {
|
|
|
153
151
|
<div class="md:col-6">
|
|
154
152
|
<a
|
|
155
153
|
class="d-block text-decoration-none"
|
|
156
|
-
href={
|
|
154
|
+
href={getDocsPath(
|
|
157
155
|
`${parentDirectory ? parentDirectory + '/' : ''}${section.slug ? section.slug : getSlug(section.title)}/`
|
|
158
156
|
)}
|
|
159
157
|
>
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Analytics
|
|
4
4
|
*
|
|
5
|
-
* Analytics script tags
|
|
5
|
+
* Google Analytics script tags. Rendered in production builds, when `analytics.googleId` is
|
|
6
|
+
* set in `config.yml`.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { getConfig } from '
|
|
9
|
+
import { getConfig } from '../../site'
|
|
10
|
+
|
|
9
11
|
const isProd = import.meta.env.PROD
|
|
10
|
-
const googleId = getConfig().analytics
|
|
12
|
+
const { googleId } = getConfig().analytics
|
|
11
13
|
---
|
|
12
14
|
|
|
13
15
|
{
|
|
14
|
-
isProd && (
|
|
16
|
+
isProd && googleId && (
|
|
15
17
|
<>
|
|
16
18
|
<script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${googleId}`} />
|
|
17
19
|
<script
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* HTML head section with meta tags, stylesheets, and SEO configuration.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { getConfig } from '
|
|
8
|
+
import { getConfig } from '../../site'
|
|
9
9
|
import type { Layout } from '../../libs/layout'
|
|
10
10
|
import Stylesheet from './Stylesheet.astro'
|
|
11
11
|
import Favicons from './Favicons.astro'
|
|
12
12
|
import Social from './Social.astro'
|
|
13
13
|
import Analytics from './Analytics.astro'
|
|
14
|
-
import '
|
|
14
|
+
import 'virtual:chassis-docs/styles'
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
17
17
|
/** Page description for meta tags */
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
* Social media meta tags (Open Graph, Twitter Cards).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import { getConfig } from '@libs/config'
|
|
10
|
-
import { getDocsPublicFsPath } from '@libs/path'
|
|
11
8
|
import { getRemoteImageSize, getStaticImageSize } from '../../libs/image'
|
|
12
9
|
import type { Layout } from '../../libs/layout'
|
|
10
|
+
import { getConfig, getPublicFsPath } from '../../site'
|
|
13
11
|
|
|
14
12
|
interface Props {
|
|
15
13
|
/** Page description */
|
|
@@ -28,12 +26,13 @@ const socialImageUrl = new URL(thumbnail, Astro.site)
|
|
|
28
26
|
const isRemoteThumbnail = /^https?:\/\//i.test(thumbnail)
|
|
29
27
|
const socialImageSize = isRemoteThumbnail
|
|
30
28
|
? await getRemoteImageSize(thumbnail)
|
|
31
|
-
: await getStaticImageSize(
|
|
29
|
+
: await getStaticImageSize(getPublicFsPath(thumbnail))
|
|
30
|
+
const { xUsername } = getConfig()
|
|
32
31
|
---
|
|
33
32
|
|
|
34
33
|
<meta name="twitter:card" content="summary_lg_image" />
|
|
35
|
-
<meta name="twitter:site" content
|
|
36
|
-
<meta name="twitter:creator" content
|
|
34
|
+
{xUsername && <meta name="twitter:site" content={`@${xUsername}`} />}
|
|
35
|
+
{xUsername && <meta name="twitter:creator" content={`@${xUsername}`} />}
|
|
37
36
|
<meta name="twitter:title" content={title} />
|
|
38
37
|
<meta name="twitter:description" content={description} />
|
|
39
38
|
<meta name="twitter:image" content={socialImageUrl} />
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Main site navigation bar with menu items and theme toggler.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { getConfig } from '@libs/config'
|
|
9
8
|
import type { Layout } from '../../libs/layout'
|
|
9
|
+
import { getConfig } from '../../site'
|
|
10
10
|
import NavLink from '../../components/NavLink.astro'
|
|
11
11
|
import ThemeToggler from '../../components/ThemeToggler.astro'
|
|
12
12
|
import SearchTrigger from './SearchTrigger.astro'
|
|
@@ -19,6 +19,7 @@ interface Props {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const { layout } = Astro.props
|
|
22
|
+
const { figmaHandle, githubOrg } = getConfig()
|
|
22
23
|
const { pathname } = Astro.url
|
|
23
24
|
const isActive = (path: string) => {
|
|
24
25
|
if (path === '/') {
|
|
@@ -106,24 +107,32 @@ const isActive = (path: string) => {
|
|
|
106
107
|
<hr class="lg:d-none m-0" />
|
|
107
108
|
|
|
108
109
|
<ul class="navbar-nav flex-row flex-wrap lg:ms-auto align-items-center">
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
{
|
|
111
|
+
figmaHandle && (
|
|
112
|
+
<NavLink
|
|
113
|
+
class="px-0 lg:px-xs"
|
|
114
|
+
href={`https://figma.com/@${figmaHandle}`}
|
|
115
|
+
target="_blank"
|
|
116
|
+
rel="noopener"
|
|
117
|
+
>
|
|
118
|
+
<Icon name="figma-brand" title="Figma" />
|
|
119
|
+
<small class="lg:d-none ms-xs">Figma Community</small>
|
|
120
|
+
</NavLink>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
{
|
|
124
|
+
githubOrg && (
|
|
125
|
+
<NavLink
|
|
126
|
+
class="px-0 lg:px-xs"
|
|
127
|
+
href={`https://github.com/${githubOrg}`}
|
|
128
|
+
target="_blank"
|
|
129
|
+
rel="noopener"
|
|
130
|
+
>
|
|
131
|
+
<Icon name="github-brand" title="GitHub" />
|
|
132
|
+
<small class="lg:d-none ms-xs">GitHub</small>
|
|
133
|
+
</NavLink>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
127
136
|
<li class="nav-item col-12 lg:col-auto">
|
|
128
137
|
<div class="vr d-none lg:d-flex h-100 lg:mx-2xs"></div>
|
|
129
138
|
<hr class="lg:d-none" />
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { load } from 'js-yaml'
|
|
4
|
+
import { z } from 'astro/zod'
|
|
5
|
+
import {
|
|
6
|
+
configSchema,
|
|
7
|
+
removedConfigKeys,
|
|
8
|
+
renamedConfigKeys,
|
|
9
|
+
sidebarSchema,
|
|
10
|
+
type ChassisConfig,
|
|
11
|
+
type Sidebar
|
|
12
|
+
} from './schema'
|
|
13
|
+
|
|
14
|
+
export interface LoadConfigOptions<TSchema extends z.ZodType = typeof configSchema> {
|
|
15
|
+
/**
|
|
16
|
+
* Path of the config file. A relative path is resolved from `root`.
|
|
17
|
+
* @default 'config.yml'
|
|
18
|
+
*/
|
|
19
|
+
file?: string
|
|
20
|
+
/**
|
|
21
|
+
* The site's root: the directory that holds `astro.config.ts`.
|
|
22
|
+
* @default process.cwd()
|
|
23
|
+
*/
|
|
24
|
+
root?: string
|
|
25
|
+
/** `configSchema`, or an extension of it when the site has keys of its own. */
|
|
26
|
+
schema?: TSchema
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getValueAtPath(record: unknown, keyPath: string): unknown {
|
|
30
|
+
return keyPath.split('.').reduce<unknown>((value, part) => {
|
|
31
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>)[part] : undefined
|
|
32
|
+
}, record)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatIssues(error: z.ZodError): string {
|
|
36
|
+
return error.issues
|
|
37
|
+
.map((issue) => ` - ${issue.path.length ? issue.path.join('.') : '(root)'}: ${issue.message}`)
|
|
38
|
+
.join('\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The schema is strict, so an old key would fail as "unrecognized key". These messages say
|
|
42
|
+
// what to do about it.
|
|
43
|
+
function findOutdatedKeys(raw: unknown): string[] {
|
|
44
|
+
const messages: string[] = []
|
|
45
|
+
|
|
46
|
+
for (const [oldKey, newKey] of Object.entries(renamedConfigKeys)) {
|
|
47
|
+
if (getValueAtPath(raw, oldKey) !== undefined) {
|
|
48
|
+
messages.push(` - \`${oldKey}\` was renamed to \`${newKey}\`.`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [oldKey, reason] of Object.entries(removedConfigKeys)) {
|
|
53
|
+
if (getValueAtPath(raw, oldKey) !== undefined) {
|
|
54
|
+
messages.push(` - \`${oldKey}\` was removed. ${reason}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return messages
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readYaml(file: string): unknown {
|
|
62
|
+
try {
|
|
63
|
+
return load(fs.readFileSync(file, 'utf8'))
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(`Failed to read \`${file}\`.`, { cause: error })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reads and validates the site's `config.yml`.
|
|
71
|
+
*
|
|
72
|
+
* For code that runs while Astro loads its configuration, such as `astro.config.ts` and a
|
|
73
|
+
* site's own integrations. Pages and components read the config with `getConfig()` from
|
|
74
|
+
* `@chassis-ui/docs/site`.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* // astro.config.ts
|
|
79
|
+
* const config = loadConfig({ root: import.meta.dirname, schema: siteConfigSchema })
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function loadConfig<TSchema extends z.ZodType = typeof configSchema>(
|
|
83
|
+
options: LoadConfigOptions<TSchema> = {}
|
|
84
|
+
): z.infer<TSchema> {
|
|
85
|
+
const { file = 'config.yml', root = process.cwd(), schema = configSchema } = options
|
|
86
|
+
const configPath = path.resolve(root, file)
|
|
87
|
+
const raw = readYaml(configPath)
|
|
88
|
+
|
|
89
|
+
const outdatedKeys = findOutdatedKeys(raw)
|
|
90
|
+
|
|
91
|
+
if (outdatedKeys.length > 0) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`\`${configPath}\` has keys from before @chassis-ui/docs 0.6.0:\n${outdatedKeys.join('\n')}`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = schema.safeParse(raw)
|
|
98
|
+
|
|
99
|
+
if (!result.success) {
|
|
100
|
+
throw new Error(`\`${configPath}\` is invalid:\n${formatIssues(result.error)}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result.data as z.infer<TSchema>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface LoadDataOptions<TSchema extends z.ZodType> {
|
|
107
|
+
/** Path of the data file. A relative path is resolved from `root`. */
|
|
108
|
+
file: string
|
|
109
|
+
/** @default process.cwd() */
|
|
110
|
+
root?: string
|
|
111
|
+
schema: TSchema
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reads and validates a YAML data file. */
|
|
115
|
+
export function loadData<TSchema extends z.ZodType>(
|
|
116
|
+
options: LoadDataOptions<TSchema>
|
|
117
|
+
): z.infer<TSchema> {
|
|
118
|
+
const { file, root = process.cwd(), schema } = options
|
|
119
|
+
const dataPath = path.resolve(root, file)
|
|
120
|
+
const result = schema.safeParse(readYaml(dataPath))
|
|
121
|
+
|
|
122
|
+
if (!result.success) {
|
|
123
|
+
throw new Error(`\`${dataPath}\` is invalid:\n${formatIssues(result.error)}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result.data as z.infer<TSchema>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Reads and validates the site's `data/sidebar.yml`. */
|
|
130
|
+
export function loadSidebar(options: { file?: string; root?: string } = {}): Sidebar {
|
|
131
|
+
return loadData({
|
|
132
|
+
file: options.file ?? 'data/sidebar.yml',
|
|
133
|
+
root: options.root,
|
|
134
|
+
schema: sidebarSchema
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type { ChassisConfig, Sidebar }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL path of a docs page.
|
|
3
|
+
*
|
|
4
|
+
* @param docsPath The `docsPath` of the site's `config.yml`, e.g. `"/css/docs"`.
|
|
5
|
+
* @param inputPath Path of the page inside the docs, e.g. `"/components/button/"`.
|
|
6
|
+
*/
|
|
7
|
+
export function joinDocsPath(docsPath: string, inputPath: string): string {
|
|
8
|
+
const base = docsPath.replace(/\/+$/, '')
|
|
9
|
+
const page = inputPath.replace(/^\/+/, '')
|
|
10
|
+
|
|
11
|
+
return page ? `${base}/${page}` : base
|
|
12
|
+
}
|
package/src/libs/highlight.ts
CHANGED
|
@@ -14,32 +14,6 @@ const classTransformer: ShikiTransformer = {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Shiki transformer that wraps non-comment lines in a `<span class="line">`
|
|
19
|
-
* element. Applied only to shell languages (bash, sh, powershell) where line
|
|
20
|
-
* wrapping is needed for the copy-button to correctly extract individual commands.
|
|
21
|
-
*/
|
|
22
|
-
const lineWrapperTransformer: ShikiTransformer = {
|
|
23
|
-
name: 'line-wrapper',
|
|
24
|
-
line(node) {
|
|
25
|
-
const hasOnlyComments = node.children.every(
|
|
26
|
-
(child) =>
|
|
27
|
-
child.type === 'element' &&
|
|
28
|
-
child.properties?.class &&
|
|
29
|
-
Array.isArray(child.properties.class) &&
|
|
30
|
-
child.properties.class.some((cls) => typeof cls === 'string' && cls.includes('comment'))
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
if (!hasOnlyComments) {
|
|
34
|
-
node.properties = node.properties || {}
|
|
35
|
-
const existing = node.properties.class ?? ''
|
|
36
|
-
const classes = typeof existing === 'string' ? existing.split(' ') : []
|
|
37
|
-
if (!classes.includes('line')) classes.push('line')
|
|
38
|
-
node.properties.class = classes.join(' ').trim()
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
17
|
/**
|
|
44
18
|
* Replaces any remaining `shiki` / `shiki-themes` class strings in the
|
|
45
19
|
* serialized HTML that the transformer may have missed (e.g. in inline styles
|
|
@@ -53,38 +27,24 @@ function replaceShikiClasses(html: string): string {
|
|
|
53
27
|
}
|
|
54
28
|
|
|
55
29
|
/**
|
|
56
|
-
* Highlights a code string using Shiki with the GitHub light/dark dual theme.
|
|
57
|
-
*
|
|
58
|
-
* Inline styles are converted to CSS classes via `transformerStyleToClass` so
|
|
59
|
-
* that a single `<style>` block can be emitted per page rather than repeating
|
|
60
|
-
* styles on every token. The returned `css` string must be injected into the
|
|
61
|
-
* page (e.g. inside a `<style>` tag) for the highlighted output to render correctly.
|
|
62
|
-
*
|
|
63
|
-
* For shell languages (`bash`, `sh`, `powershell`) each non-comment line is
|
|
64
|
-
* additionally wrapped in `<span class="line">` to support per-line copy.
|
|
30
|
+
* Highlights a code string using Shiki with the GitHub light/dark dual theme. Shiki wraps
|
|
31
|
+
* each line in a `<span class="line">`.
|
|
65
32
|
*
|
|
66
33
|
* @param code - Source code to highlight.
|
|
67
34
|
* @param lang - Shiki language identifier (e.g. `"html"`, `"ts"`, `"bash"`).
|
|
68
35
|
* @param extraTransformers - Additional Shiki transformers to apply before the built-ins.
|
|
69
|
-
* @returns An object with `html
|
|
36
|
+
* @returns An object with `html`, the highlighted markup.
|
|
70
37
|
*/
|
|
71
38
|
export async function highlightCode(
|
|
72
39
|
code: string,
|
|
73
40
|
lang: string,
|
|
74
41
|
extraTransformers: ShikiTransformer[] = []
|
|
75
42
|
): Promise<{ html: string }> {
|
|
76
|
-
const shouldWrapLines = ['bash', 'sh', 'powershell'].includes(lang)
|
|
77
|
-
|
|
78
|
-
const transformers: ShikiTransformer[] = [...extraTransformers, classTransformer]
|
|
79
|
-
if (shouldWrapLines) {
|
|
80
|
-
transformers.push(lineWrapperTransformer)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
43
|
const highlighted = await codeToHtml(code, {
|
|
84
44
|
lang,
|
|
85
45
|
themes: DOCS_SHIKI_THEMES,
|
|
86
46
|
defaultColor: false,
|
|
87
|
-
transformers
|
|
47
|
+
transformers: [...extraTransformers, classTransformer]
|
|
88
48
|
})
|
|
89
49
|
|
|
90
50
|
return { html: replaceShikiClasses(highlighted) }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
// Public paths of the package and the directories behind them. They match `exports` in
|
|
6
|
+
// `package.json`.
|
|
7
|
+
const publicDirectories: Record<string, string> = {
|
|
8
|
+
components: 'src/components',
|
|
9
|
+
js: 'src/js',
|
|
10
|
+
layouts: 'src/layouts',
|
|
11
|
+
scss: 'src/scss',
|
|
12
|
+
shortcodes: 'src/components/shortcodes'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Absolute path of the package on disk.
|
|
17
|
+
*
|
|
18
|
+
* Only correct in code that runs while Astro loads its configuration. Pages and components
|
|
19
|
+
* are bundled, so they use `getPackageFilePath()` from `@chassis-ui/docs/site`.
|
|
20
|
+
*/
|
|
21
|
+
export function getPackageRoot(): string {
|
|
22
|
+
return fileURLToPath(new URL('../../', import.meta.url))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Absolute path of a file of the package, named by its public path.
|
|
27
|
+
*
|
|
28
|
+
* @param file Path as it is imported, without the package name, e.g. `"js/color-modes.js"`
|
|
29
|
+
* or `"shortcodes/Code.astro"`.
|
|
30
|
+
* @param packageRoot Absolute path of the package.
|
|
31
|
+
*/
|
|
32
|
+
export function resolvePackageFilePath(file: string, packageRoot = getPackageRoot()): string {
|
|
33
|
+
const [directory, ...rest] = file.split('/')
|
|
34
|
+
const target = publicDirectories[directory]
|
|
35
|
+
|
|
36
|
+
if (!target) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`'${file}' is not a public path of @chassis-ui/docs. Expected a path that starts with one of: ${Object.keys(publicDirectories).join(', ')}.`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return path.join(packageRoot, target, ...rest)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The repository root is the nearest ancestor with a `.git` entry or a pnpm workspace file.
|
|
46
|
+
function getSearchDirectories(root: string): string[] {
|
|
47
|
+
const directories: string[] = []
|
|
48
|
+
let current = path.resolve(root)
|
|
49
|
+
|
|
50
|
+
while (true) {
|
|
51
|
+
directories.push(current)
|
|
52
|
+
|
|
53
|
+
const isRepositoryRoot =
|
|
54
|
+
fs.existsSync(path.join(current, '.git')) ||
|
|
55
|
+
fs.existsSync(path.join(current, 'pnpm-workspace.yaml'))
|
|
56
|
+
const parent = path.dirname(current)
|
|
57
|
+
|
|
58
|
+
if (isRepositoryRoot || parent === current) {
|
|
59
|
+
return directories
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
current = parent
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Every `node_modules` directory between the site's root and the root of the repository,
|
|
68
|
+
* nearest first. For tools that resolve package names without Vite, such as the Sass API.
|
|
69
|
+
*/
|
|
70
|
+
export function getNodeModulesFsPaths(root = process.cwd()): string[] {
|
|
71
|
+
return getSearchDirectories(root)
|
|
72
|
+
.map((directory) => path.join(directory, 'node_modules'))
|
|
73
|
+
.filter((directory) => fs.existsSync(directory))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function findUp(root: string, candidates: string[], description: string): string {
|
|
77
|
+
const searched = getSearchDirectories(root)
|
|
78
|
+
|
|
79
|
+
for (const directory of searched) {
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
const candidatePath = path.join(directory, candidate)
|
|
82
|
+
|
|
83
|
+
if (fs.existsSync(candidatePath)) {
|
|
84
|
+
return candidatePath
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Could not find ${description}. Looked for ${candidates.map((c) => `'${c}'`).join(' or ')} in:\n${searched.map((d) => ` - ${d}`).join('\n')}`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ChassisPathOptions {
|
|
95
|
+
/**
|
|
96
|
+
* The site's root. The search starts here and stops at the root of the repository.
|
|
97
|
+
* @default process.cwd()
|
|
98
|
+
*/
|
|
99
|
+
root?: string
|
|
100
|
+
/**
|
|
101
|
+
* The directory to use, when it is not where the search would find it. A relative path is
|
|
102
|
+
* resolved from `root`.
|
|
103
|
+
*/
|
|
104
|
+
dir?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveDir({ root = process.cwd(), dir }: ChassisPathOptions): string | undefined {
|
|
108
|
+
return dir ? path.resolve(root, dir) : undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Directory of an installed package. Finds the package whether `node_modules` is in the
|
|
113
|
+
* site's root or hoisted to the root of the repository.
|
|
114
|
+
*/
|
|
115
|
+
export function getInstalledPackageFsPath(name: string, root = process.cwd()): string {
|
|
116
|
+
return findUp(root, [path.join('node_modules', name)], `the package '${name}'`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The docs build of `@chassis-ui/tokens`. The folder moved from `dist/tokens/web/docs` to
|
|
121
|
+
* `dist/web/docs` between releases of the tokens package, and both are found.
|
|
122
|
+
*/
|
|
123
|
+
export function getChassisTokensFsPath(options: ChassisPathOptions = {}): string {
|
|
124
|
+
const tokens =
|
|
125
|
+
resolveDir(options) ?? getInstalledPackageFsPath('@chassis-ui/tokens', options.root)
|
|
126
|
+
|
|
127
|
+
return findUp(
|
|
128
|
+
tokens,
|
|
129
|
+
['dist/web/docs/chassis', 'dist/tokens/web/docs/chassis'],
|
|
130
|
+
'the docs build of @chassis-ui/tokens'
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The docs build of `chassis-assets`: in the `vendor/assets` submodule, or in `dist` for the
|
|
136
|
+
* site of `chassis-assets` itself.
|
|
137
|
+
*/
|
|
138
|
+
export function getChassisAssetsFsPath(options: ChassisPathOptions = {}): string {
|
|
139
|
+
return (
|
|
140
|
+
resolveDir(options) ??
|
|
141
|
+
findUp(
|
|
142
|
+
options.root ?? process.cwd(),
|
|
143
|
+
['vendor/assets/dist/web/docs/chassis', 'dist/web/docs/chassis'],
|
|
144
|
+
'the docs build of chassis-assets'
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The `dist` folder of `@chassis-ui/css`. */
|
|
150
|
+
export function getChassisCSSFsPath(options: ChassisPathOptions = {}): string {
|
|
151
|
+
return (
|
|
152
|
+
resolveDir(options) ??
|
|
153
|
+
path.join(getInstalledPackageFsPath('@chassis-ui/css', options.root), 'dist')
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The folder of `@chassis-ui/icons`. */
|
|
158
|
+
export function getChassisIconsFsPath(options: ChassisPathOptions = {}): string {
|
|
159
|
+
return resolveDir(options) ?? getInstalledPackageFsPath('@chassis-ui/icons', options.root)
|
|
160
|
+
}
|