@chassis-ui/docs 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/index.ts +6 -1
- package/package.json +20 -8
- package/src/components/DocsSidebar.astro +63 -23
- package/src/components/FeatureCard.astro +3 -3
- package/src/components/TableOfContents.astro +8 -7
- package/src/components/ThemeToggler.astro +15 -36
- package/src/components/shortcodes/Blockquote.astro +1 -1
- package/src/components/shortcodes/CSSOnly.astro +6 -2
- package/src/components/shortcodes/Callout.astro +6 -2
- package/src/components/shortcodes/Code.astro +289 -0
- package/src/components/shortcodes/DeprecatedIn.astro +5 -2
- package/src/components/shortcodes/Example.astro +132 -0
- package/src/components/shortcodes/Icon.astro +57 -0
- package/src/components/shortcodes/InFigma.astro +7 -2
- package/src/components/shortcodes/JsDocs.astro +64 -0
- package/src/components/shortcodes/Placeholder.astro +28 -0
- package/src/components/shortcodes/ResizableExample.astro +126 -0
- package/src/components/shortcodes/ScssDocs.astro +108 -0
- package/src/components/shortcodes/ScssDocsSimple.astro +66 -0
- package/src/js/color-modes.js +3 -3
- package/src/js/example-mode.js +75 -0
- package/src/js/search.js +501 -39
- package/src/js/theme-color.js +1 -1
- package/src/layouts/DocsLayout.astro +11 -14
- package/src/layouts/RedirectLayout.astro +1 -0
- package/src/layouts/footer/Scripts.astro +14 -9
- package/src/layouts/head/Analytics.astro +12 -6
- package/src/layouts/head/Favicons.astro +0 -2
- package/src/layouts/head/Head.astro +2 -7
- package/src/layouts/head/Stylesheet.astro +18 -11
- package/src/layouts/header/Header.astro +2 -5
- package/src/layouts/header/Navigation.astro +31 -33
- package/src/layouts/header/SearchDialog.astro +74 -0
- package/src/layouts/header/SearchTrigger.astro +41 -0
- package/src/libs/highlight.ts +91 -0
- package/src/libs/icon.ts +75 -0
- package/src/libs/image.ts +12 -1
- package/src/libs/layout.ts +18 -0
- package/src/libs/markdown.ts +128 -0
- package/src/libs/placeholder.ts +250 -0
- package/src/libs/rehype.ts +0 -17
- package/src/libs/shortcodes.ts +129 -0
- package/src/libs/site.ts +95 -0
- package/src/libs/toc.ts +40 -6
- package/src/libs/utils.ts +34 -82
- package/src/libs/vite.ts +45 -0
- package/src/scss/_anchor.scss +20 -16
- package/src/scss/_callouts.scss +29 -28
- package/src/scss/_content.scss +72 -103
- package/src/scss/_footer.scss +18 -19
- package/src/scss/_layout.scss +56 -63
- package/src/scss/_navbar.scss +19 -123
- package/src/scss/_placeholder-img.scss +11 -7
- package/src/scss/_scrolling.scss +14 -12
- package/src/scss/_search.scss +418 -0
- package/src/scss/_shiki.scss +120 -0
- package/src/scss/_sidebar.scss +137 -132
- package/src/scss/_skippy.scss +6 -4
- package/src/scss/_snippets.scss +181 -0
- package/src/scss/_toc.scss +97 -93
- package/src/scss/_vars.scss +89 -0
- package/src/scss/main.scss +75 -80
- package/src/js/icon-loader.js +0 -229
- package/src/libs/chassis.ts +0 -38
- package/src/scss/_brand.scss +0 -59
- package/src/scss/_buttons.scss +0 -33
- package/src/scss/_clipboard-js.scss +0 -63
- package/src/scss/_code.scss +0 -116
- package/src/scss/_colors.scss +0 -140
- package/src/scss/_docsearch.scss +0 -212
- package/src/scss/_settings.scss +0 -29
- package/src/scss/_syntax.scss +0 -160
- package/src/scss/_variables.scss +0 -73
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { AstroUserConfig } from 'astro'
|
|
2
|
+
import type { Element, Text } from 'hast'
|
|
3
|
+
import type { ShikiTransformer } from 'shiki'
|
|
4
|
+
import { rehypeHeadingIds, unified } from '@astrojs/markdown-remark'
|
|
5
|
+
import type { UnifiedProcessorOptions } from '@astrojs/markdown-remark'
|
|
6
|
+
import { transformerNotationDiff, transformerNotationHighlight } from '@shikijs/transformers'
|
|
7
|
+
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
|
8
|
+
import { rehypeCxTable } from './rehype'
|
|
9
|
+
|
|
10
|
+
type MarkdownConfig = NonNullable<AstroUserConfig['markdown']>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shiki theme pair used for both programmatic highlighting (`highlightCode`) and
|
|
14
|
+
* the Astro markdown pipeline (`getDocsMarkdownConfig`). Defined once here to
|
|
15
|
+
* keep both in sync.
|
|
16
|
+
*/
|
|
17
|
+
export const DOCS_SHIKI_THEMES = {
|
|
18
|
+
light: 'github-light' as const,
|
|
19
|
+
dark: 'github-dark' as const
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Shiki transformer that exposes the current language as a `data-language`
|
|
24
|
+
* attribute on `<pre>` elements. The `Code` component reads this attribute when
|
|
25
|
+
* rendering inline markdown code blocks so it can display the correct language
|
|
26
|
+
* label without requiring an explicit `lang` prop.
|
|
27
|
+
*/
|
|
28
|
+
const addLanguageAttributeTransformer: ShikiTransformer = {
|
|
29
|
+
name: 'add-language-attribute',
|
|
30
|
+
pre(node) {
|
|
31
|
+
const lang = this.options.lang
|
|
32
|
+
if (lang) {
|
|
33
|
+
node.properties['dataLanguage'] = lang
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options accepted by `getDocsMarkdownConfig`.
|
|
40
|
+
*/
|
|
41
|
+
export interface DocsMarkdownConfigOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Heading depth range used to determine which headings receive an autolink
|
|
44
|
+
* anchor. Typically sourced from the project's `config.yml` `anchors` section.
|
|
45
|
+
*
|
|
46
|
+
* @example { min: 2, max: 4 } // anchors h2–h4
|
|
47
|
+
*/
|
|
48
|
+
anchors: {
|
|
49
|
+
min: number
|
|
50
|
+
max: number
|
|
51
|
+
}
|
|
52
|
+
/** Additional project-specific rehype plugins, appended after the base set. */
|
|
53
|
+
rehypePlugins?: UnifiedProcessorOptions['rehypePlugins']
|
|
54
|
+
/** Additional project-specific remark plugins, appended after the base set. */
|
|
55
|
+
remarkPlugins?: UnifiedProcessorOptions['remarkPlugins']
|
|
56
|
+
/**
|
|
57
|
+
* Passed directly to `remark-rehype`. Shallow-merged so individual handler
|
|
58
|
+
* overrides don't discard base handlers.
|
|
59
|
+
*/
|
|
60
|
+
remarkRehype?: UnifiedProcessorOptions['remarkRehype']
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the shared Astro markdown configuration for Chassis docs sites.
|
|
65
|
+
*
|
|
66
|
+
* Includes the following base rehype plugins (in order):
|
|
67
|
+
* - `rehypeHeadingIds` — adds `id` attributes to all headings.
|
|
68
|
+
* - `rehypeAutolinkHeadings` — appends anchor links to headings in the
|
|
69
|
+
* `anchors.min`–`anchors.max` depth range.
|
|
70
|
+
* - `rehypeCxTable` — applies CSS classes to `<CxTable>`-wrapped tables.
|
|
71
|
+
* - _(any `options.rehypePlugins`)_
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* // astro.config.ts
|
|
76
|
+
* import { getDocsMarkdownConfig } from '@chassis-ui/docs'
|
|
77
|
+
* import { getConfig } from './src/libs/config'
|
|
78
|
+
*
|
|
79
|
+
* export default defineConfig({
|
|
80
|
+
* markdown: getDocsMarkdownConfig({
|
|
81
|
+
* anchors: getConfig().anchors,
|
|
82
|
+
* rehypePlugins: [myExtraPlugin],
|
|
83
|
+
* remarkPlugins: [remarkCxConfig, remarkDefinitionList],
|
|
84
|
+
* remarkRehype: { handlers: defListHastHandlers }
|
|
85
|
+
* })
|
|
86
|
+
* })
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function getDocsMarkdownConfig(options: DocsMarkdownConfigOptions): MarkdownConfig {
|
|
90
|
+
const headingsRangeRegex = new RegExp(`^h[${options.anchors.min}-${options.anchors.max}]$`)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
processor: unified({
|
|
94
|
+
smartypants: false,
|
|
95
|
+
rehypePlugins: [
|
|
96
|
+
rehypeHeadingIds,
|
|
97
|
+
[
|
|
98
|
+
rehypeAutolinkHeadings,
|
|
99
|
+
{
|
|
100
|
+
behavior: 'append',
|
|
101
|
+
content: [{ type: 'text', value: ' ' }],
|
|
102
|
+
properties: (element: Element) => ({
|
|
103
|
+
class: 'anchor-link',
|
|
104
|
+
ariaLabel: `Link to this section: ${(element.children[0] as Text).value}`
|
|
105
|
+
}),
|
|
106
|
+
test: (element: Element) => element.tagName.match(headingsRangeRegex)
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
rehypeCxTable,
|
|
110
|
+
...(options.rehypePlugins ?? [])
|
|
111
|
+
],
|
|
112
|
+
remarkPlugins: options.remarkPlugins ?? [],
|
|
113
|
+
...(options.remarkRehype && {
|
|
114
|
+
remarkRehype: options.remarkRehype
|
|
115
|
+
})
|
|
116
|
+
}),
|
|
117
|
+
syntaxHighlight: 'shiki',
|
|
118
|
+
shikiConfig: {
|
|
119
|
+
themes: DOCS_SHIKI_THEMES,
|
|
120
|
+
defaultColor: false,
|
|
121
|
+
transformers: [
|
|
122
|
+
transformerNotationDiff(),
|
|
123
|
+
transformerNotationHighlight(),
|
|
124
|
+
addLanguageAttributeTransformer
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
2
|
+
import * as htmlparser2 from 'htmlparser2'
|
|
3
|
+
|
|
4
|
+
const placeholderRegex = /<Placeholder\s+([^>]+)\/>/g
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates all the placeholder attributes and options required to render a placeholder.
|
|
8
|
+
* @see src/components/shortcodes/Placeholder.astro
|
|
9
|
+
*/
|
|
10
|
+
export function getPlaceholder(userOptions: Partial<PlaceholderOptions>): Placeholder {
|
|
11
|
+
const options = getOptionsWithDefaults(userOptions)
|
|
12
|
+
const { class: className, height, markup, text, title, width } = options
|
|
13
|
+
|
|
14
|
+
const showText = text !== false
|
|
15
|
+
const showTitle = title !== false
|
|
16
|
+
|
|
17
|
+
const placeholderClassList = ['cxd-placeholder-image', className].join(' ')
|
|
18
|
+
const placeholderRole = showTitle || showText ? 'img' : undefined
|
|
19
|
+
const placeholderAriaHidden = !showText && !showTitle ? 'true' : undefined
|
|
20
|
+
|
|
21
|
+
const placeholderLabel =
|
|
22
|
+
showText || showTitle
|
|
23
|
+
? `${showTitle ? title : ''}${showTitle && showText ? ': ' : ''}${showText ? text : ''}`
|
|
24
|
+
: undefined
|
|
25
|
+
|
|
26
|
+
const optionsWithVisibilities = { ...options, showText, showTitle }
|
|
27
|
+
|
|
28
|
+
if (markup === 'img') {
|
|
29
|
+
return {
|
|
30
|
+
type: 'img',
|
|
31
|
+
options: optionsWithVisibilities,
|
|
32
|
+
props: {
|
|
33
|
+
alt: placeholderLabel,
|
|
34
|
+
class: placeholderClassList,
|
|
35
|
+
height,
|
|
36
|
+
src: getPlaceholderSrc(showTitle, showText, options),
|
|
37
|
+
width
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
type: 'svg',
|
|
44
|
+
options: optionsWithVisibilities,
|
|
45
|
+
props: {
|
|
46
|
+
'aria-hidden': placeholderAriaHidden,
|
|
47
|
+
'aria-label': placeholderLabel,
|
|
48
|
+
class: placeholderClassList,
|
|
49
|
+
height,
|
|
50
|
+
preserveAspectRatio: 'xMidYMid slice',
|
|
51
|
+
role: placeholderRole,
|
|
52
|
+
width,
|
|
53
|
+
xmlns: 'http://www.w3.org/2000/svg'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Replaces placeholders described using the `<Placeholder />` component in HTML markup with the expected HTML content.
|
|
60
|
+
* This is useful to render examples that have a pretty large set of constraints:
|
|
61
|
+
*
|
|
62
|
+
* - The provided HTML code is not valid MDX (e.g. unclosed void elements like <img>) but can contain the
|
|
63
|
+
* `<Placeholder />` Astro component. This means that we cannot use an Astro slot for example that requires valid
|
|
64
|
+
* MDX.
|
|
65
|
+
* - The provided HTML code cannot be parsed in a forgiving way with XML mode enabled (to not lose the structure due
|
|
66
|
+
* to self-closing MDX or Astro components) and serialized back to a string while closing all known void elements
|
|
67
|
+
* in order to render it as MDX using `@mdx-js/mdx` & `astro/jsx-runtime`. This works perfectly (tested) but the
|
|
68
|
+
* DOM needs to contains the exact same HTML markup (even indentation) provided to the example as it is used on the
|
|
69
|
+
* client to send the example to StackBlitz with the same indentation as the original example.
|
|
70
|
+
*
|
|
71
|
+
* If you are not sure if you need to use this function, you probably don't.
|
|
72
|
+
*/
|
|
73
|
+
export function replacePlaceholdersInHtml(html: string) {
|
|
74
|
+
return html.replace(placeholderRegex, (match) => {
|
|
75
|
+
const document = htmlparser2.parseDocument(match, { xmlMode: true })
|
|
76
|
+
const placeholderElement = document.firstChild
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
document.children.length > 1 ||
|
|
80
|
+
!placeholderElement ||
|
|
81
|
+
placeholderElement.type !== htmlparser2.ElementType.Tag ||
|
|
82
|
+
placeholderElement.name !== 'Placeholder'
|
|
83
|
+
) {
|
|
84
|
+
throw new Error('Invalid placeholder element.')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const placeholder = getPlaceholder(sanitizeHtmlAttributesFromMdx(placeholderElement.attribs))
|
|
88
|
+
|
|
89
|
+
return renderPlaceholderToString(placeholder)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderPlaceholderToString(placeholder: Placeholder) {
|
|
94
|
+
let placeholderStr = `<${placeholder.type}`
|
|
95
|
+
|
|
96
|
+
for (const [key, value] of Object.entries(placeholder.props)) {
|
|
97
|
+
if (value === undefined) {
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
placeholderStr = `${placeholderStr} ${key}="${value}"`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (placeholder.type === 'img') {
|
|
105
|
+
return `${placeholderStr} />`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
placeholderStr = `${placeholderStr}>`
|
|
109
|
+
|
|
110
|
+
if (placeholder.options.showTitle) {
|
|
111
|
+
placeholderStr = `${placeholderStr}<title>${placeholder.options.title}</title>`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
placeholderStr = `${placeholderStr}<rect width="100%" height="100%" fill="${placeholder.options.background}" />`
|
|
115
|
+
|
|
116
|
+
if (placeholder.options.showText) {
|
|
117
|
+
placeholderStr = `${placeholderStr}<text x="50%" y="50%" fill="${placeholder.options.color}" dy=".3em">${placeholder.options.text}</text>`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `${placeholderStr}</${placeholder.type}>`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getOptionsWithDefaults(options: Partial<PlaceholderOptions>) {
|
|
124
|
+
const optionsWithDefaults = Object.assign(
|
|
125
|
+
{},
|
|
126
|
+
{
|
|
127
|
+
background: 'var(--cx-neutral-bg-evident)',
|
|
128
|
+
color: 'var(--cx-neutral-fg-subtle)',
|
|
129
|
+
// width: '100%',
|
|
130
|
+
// height: '180',
|
|
131
|
+
markup: 'svg',
|
|
132
|
+
title: 'Placeholder'
|
|
133
|
+
},
|
|
134
|
+
options
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if (optionsWithDefaults.text === undefined) {
|
|
138
|
+
optionsWithDefaults.text = `${optionsWithDefaults.width}x${optionsWithDefaults.height}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return optionsWithDefaults as PlaceholderOptions
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getPlaceholderSrc(
|
|
145
|
+
showTitle: boolean,
|
|
146
|
+
showText: boolean,
|
|
147
|
+
{ background, color, text, title }: PlaceholderOptions
|
|
148
|
+
) {
|
|
149
|
+
// Sanitize the background and text colors by removing the leading hash if any.
|
|
150
|
+
const bgColor = background.replace(/^#/, '')
|
|
151
|
+
const textColor = color.replace(/^#/, '')
|
|
152
|
+
|
|
153
|
+
// Build the raw SVG string first
|
|
154
|
+
let svg = `<svg style='font-size: 1.125rem; font-family:system-ui; -webkit-user-select: none; -moz-user-select: none; user-select: none; text-anchor: middle;' width='200' height='200' xmlns='http://www.w3.org/2000/svg'>`
|
|
155
|
+
|
|
156
|
+
if (showTitle) {
|
|
157
|
+
svg += `<title>${title}</title>`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
svg += `<rect width='100%' height='100%' fill='#${bgColor}'></rect>`
|
|
161
|
+
|
|
162
|
+
if (showText) {
|
|
163
|
+
svg += `<text x='50%' y='50%' fill='#${textColor}' dy='.3em'>${text}</text>`
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
svg += `</svg>`
|
|
167
|
+
|
|
168
|
+
const encodedSvg = encodeURIComponent(svg)
|
|
169
|
+
|
|
170
|
+
return `data:image/svg+xml,${encodedSvg}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sanitizeHtmlAttributesFromMdx(attributes: Record<string, unknown>) {
|
|
174
|
+
const sanitizedAttributes: typeof attributes = {}
|
|
175
|
+
|
|
176
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
177
|
+
if (value === undefined) {
|
|
178
|
+
continue
|
|
179
|
+
} else if (value === '{false}') {
|
|
180
|
+
sanitizedAttributes[key] = false
|
|
181
|
+
} else if (value === '{true}') {
|
|
182
|
+
sanitizedAttributes[key] = true
|
|
183
|
+
} else {
|
|
184
|
+
sanitizedAttributes[key] = value
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return sanitizedAttributes
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface PlaceholderOptions {
|
|
192
|
+
/**
|
|
193
|
+
* The SVG background color.
|
|
194
|
+
* @default "#868f96"
|
|
195
|
+
*/
|
|
196
|
+
background: string
|
|
197
|
+
/**
|
|
198
|
+
* CSS classes to append to `cxd-placeholder-img` for the `svg` or `img` elements.
|
|
199
|
+
*/
|
|
200
|
+
class?: string
|
|
201
|
+
/**
|
|
202
|
+
* The text color (foreground).
|
|
203
|
+
* @default "#dee2e6"
|
|
204
|
+
*/
|
|
205
|
+
color: string
|
|
206
|
+
/**
|
|
207
|
+
* The placeholder height.
|
|
208
|
+
* @default "180"
|
|
209
|
+
*/
|
|
210
|
+
height: string
|
|
211
|
+
/**
|
|
212
|
+
* If it should render `svg` or `img` tags.
|
|
213
|
+
* @default "svg"
|
|
214
|
+
*/
|
|
215
|
+
markup: 'img' | 'svg'
|
|
216
|
+
/**
|
|
217
|
+
* The text to show in the image. You can explicitely pass the `false` boolean value (and not the string "false") to
|
|
218
|
+
* hide the text.
|
|
219
|
+
* @default "${width}x${height}"
|
|
220
|
+
*/
|
|
221
|
+
text: string | false
|
|
222
|
+
/**
|
|
223
|
+
* Used in the SVG `title` tag. You can explicitely pass the `false` boolean value (and not the string "false") to
|
|
224
|
+
* hide the title.
|
|
225
|
+
* @default "Placeholder"
|
|
226
|
+
*/
|
|
227
|
+
title: string | false
|
|
228
|
+
/**
|
|
229
|
+
* The placeholder width.
|
|
230
|
+
* @default "100%"
|
|
231
|
+
*/
|
|
232
|
+
width: string
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface PlaceholderVisibilities {
|
|
236
|
+
showText: boolean
|
|
237
|
+
showTitle: boolean
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type Placeholder =
|
|
241
|
+
| {
|
|
242
|
+
type: 'img'
|
|
243
|
+
options: PlaceholderOptions & PlaceholderVisibilities
|
|
244
|
+
props: HTMLAttributes<'img'>
|
|
245
|
+
}
|
|
246
|
+
| {
|
|
247
|
+
type: 'svg'
|
|
248
|
+
options: PlaceholderOptions & PlaceholderVisibilities
|
|
249
|
+
props: HTMLAttributes<'svg'>
|
|
250
|
+
}
|
package/src/libs/rehype.ts
CHANGED
|
@@ -41,20 +41,3 @@ export const rehypeCxTable: Plugin<[], Root> = function () {
|
|
|
41
41
|
})
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Rehype plugin to strip `is:raw` attributes from code elements.
|
|
47
|
-
*
|
|
48
|
-
* The `@astrojs/markdown-remark` rehype-prism plugin adds `is:raw` as an Astro
|
|
49
|
-
* directive, but it leaks into the final HTML output because rehype processes
|
|
50
|
-
* it as a plain attribute rather than an Astro compiler directive.
|
|
51
|
-
*/
|
|
52
|
-
export const rehypeStripIsRaw: Plugin<[], Root> = function () {
|
|
53
|
-
return function rehypeStripIsRawPlugin(tree) {
|
|
54
|
-
visit(tree, 'element', (node) => {
|
|
55
|
-
if (node.properties && 'is:raw' in node.properties) {
|
|
56
|
-
delete node.properties['is:raw']
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { AstroIntegration } from 'astro'
|
|
4
|
+
|
|
5
|
+
interface MdxjsEsmNode {
|
|
6
|
+
type: 'mdxjsEsm'
|
|
7
|
+
value: string
|
|
8
|
+
data: { estree: object }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildImportsNode(imports: string[]): MdxjsEsmNode {
|
|
12
|
+
const body = imports.map((filePath) => {
|
|
13
|
+
const name = path.parse(filePath).name.replaceAll(/[^\w\d]/g, '')
|
|
14
|
+
return {
|
|
15
|
+
type: 'ImportDeclaration',
|
|
16
|
+
specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name } }],
|
|
17
|
+
source: { type: 'Literal', value: filePath, raw: JSON.stringify(filePath) }
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
type: 'mdxjsEsm',
|
|
23
|
+
value: '',
|
|
24
|
+
data: { estree: { type: 'Program', sourceType: 'module', body } }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ChassisAutoImportOptions {
|
|
29
|
+
/** Absolute path to the docs site root (e.g. `getDocsFsPath()`). Used for the
|
|
30
|
+
* site-local shortcodes directory and the type definitions output path. */
|
|
31
|
+
docsPath: string
|
|
32
|
+
/** Absolute path to the directory containing `node_modules`. Defaults to
|
|
33
|
+
* `docsPath` when omitted — override when `node_modules` lives at the repo root. */
|
|
34
|
+
modulesPath?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scanComponents({ docsPath, modulesPath = docsPath }: ChassisAutoImportOptions) {
|
|
38
|
+
const dirs = [
|
|
39
|
+
path.join(modulesPath, 'node_modules/@chassis-ui/docs/src/components/shortcodes'),
|
|
40
|
+
path.join(docsPath, 'src/components/shortcodes')
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const components: string[] = []
|
|
44
|
+
const definitions: string[] = []
|
|
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
|
+
}
|
|
61
|
+
|
|
62
|
+
return { components, definitions }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Astro integration that writes the auto-import type definitions file, and a
|
|
67
|
+
* remark plugin factory for injecting shortcode imports into every MDX file.
|
|
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
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function chassisAutoImport(options: ChassisAutoImportOptions): {
|
|
82
|
+
/** Astro integration that writes `src/types/auto-import.d.ts`. */
|
|
83
|
+
integration: () => AstroIntegration
|
|
84
|
+
/** Factory for the remark plugin that injects imports into every `.mdx` file. */
|
|
85
|
+
plugin: () => ReturnType<typeof createAutoImportPlugin>
|
|
86
|
+
} {
|
|
87
|
+
const { components, definitions } = scanComponents(options)
|
|
88
|
+
const typeDefinitionsPath = path.join(options.docsPath, 'src/types/auto-import.d.ts')
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
integration: () => ({
|
|
92
|
+
name: 'chassis-auto-import',
|
|
93
|
+
hooks: {
|
|
94
|
+
'astro:config:setup': () => {
|
|
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.
|
|
102
|
+
*/
|
|
103
|
+
export declare global {
|
|
104
|
+
${definitions.join('\n ')}
|
|
105
|
+
}
|
|
106
|
+
`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}),
|
|
111
|
+
plugin: () => createAutoImportPlugin(components)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns a remark plugin that prepends import statements for the given component
|
|
117
|
+
* paths into every `.mdx` file.
|
|
118
|
+
*/
|
|
119
|
+
export function createAutoImportPlugin(imports: string[]) {
|
|
120
|
+
const importsNode = buildImportsNode(imports)
|
|
121
|
+
|
|
122
|
+
return function remarkAutoImport() {
|
|
123
|
+
return function (tree: any, vfile: any) {
|
|
124
|
+
if (!vfile.basename?.endsWith('.md')) {
|
|
125
|
+
tree.children.unshift(importsNode)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/libs/site.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import os from 'os'
|
|
2
|
+
|
|
3
|
+
/** Resolved host and port for the local Astro development server. */
|
|
4
|
+
interface DevHost {
|
|
5
|
+
/** Hostname or IP address (e.g. `"localhost"` or a LAN IP when `--host` is passed). */
|
|
6
|
+
host: string
|
|
7
|
+
/** Port number the dev server is listening on. */
|
|
8
|
+
port: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimum site configuration shape required by `getSiteUrl`.
|
|
13
|
+
* The consuming project's full config object satisfies this interface as long
|
|
14
|
+
* as it exposes a `baseURL` string.
|
|
15
|
+
*/
|
|
16
|
+
export interface SiteConfig {
|
|
17
|
+
/** Canonical base URL used as a fallback in production (e.g. `"https://chassis-ui.com/css/"`). */
|
|
18
|
+
baseURL: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the host and port for the local Astro development server.
|
|
23
|
+
*
|
|
24
|
+
* Resolution order for port: `--port` CLI flag → `PORT` env var → `4321` (Astro default).
|
|
25
|
+
* When `--host` is passed, the first non-internal IPv4 network interface address
|
|
26
|
+
* is used as the host so the dev URL is reachable on the LAN.
|
|
27
|
+
*/
|
|
28
|
+
function getDevHost(): DevHost {
|
|
29
|
+
const hasHostFlag = process.argv.includes('--host')
|
|
30
|
+
let host = 'localhost'
|
|
31
|
+
let port = 4321
|
|
32
|
+
|
|
33
|
+
const portFlagIndex = process.argv.findIndex((arg) => arg === '--port')
|
|
34
|
+
if (portFlagIndex !== -1 && process.argv[portFlagIndex + 1]) {
|
|
35
|
+
const cliPort = parseInt(process.argv[portFlagIndex + 1], 10)
|
|
36
|
+
if (!isNaN(cliPort) && cliPort > 0 && cliPort < 65536) {
|
|
37
|
+
port = cliPort
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.PORT) {
|
|
42
|
+
const envPort = parseInt(process.env.PORT, 10)
|
|
43
|
+
if (!isNaN(envPort) && envPort > 0 && envPort < 65536) {
|
|
44
|
+
port = envPort
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (hasHostFlag) {
|
|
49
|
+
const interfaces = os.networkInterfaces()
|
|
50
|
+
for (const iface of Object.values(interfaces).flat()) {
|
|
51
|
+
if (iface?.family === 'IPv4' && !iface.internal) {
|
|
52
|
+
host = iface.address
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { host, port }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Determines the canonical site URL for the current environment.
|
|
63
|
+
*
|
|
64
|
+
* Resolution order:
|
|
65
|
+
* 1. `SITE_URL` env var — explicit override, takes precedence everywhere.
|
|
66
|
+
* 2. `NODE_ENV=development` — constructs a local `http://` URL from `getDevHost()`.
|
|
67
|
+
* 3. `VERCEL_ENV=production` — uses `config.baseURL` (the path-prefixed canonical
|
|
68
|
+
* URL) rather than `VERCEL_PROJECT_PRODUCTION_URL` (the sub-project custom domain)
|
|
69
|
+
* so sitemaps always point to the correct public URL.
|
|
70
|
+
* 4. `VERCEL_URL` present — Vercel preview/branch deployment; uses the auto-generated
|
|
71
|
+
* `https://` preview URL.
|
|
72
|
+
* 5. Fallback — `config.baseURL` from the project's `config.yml`.
|
|
73
|
+
*
|
|
74
|
+
* @param config - Site configuration providing the `baseURL` fallback.
|
|
75
|
+
*/
|
|
76
|
+
export function getSiteUrl(config: SiteConfig): string {
|
|
77
|
+
if (process.env.SITE_URL) {
|
|
78
|
+
return process.env.SITE_URL
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (process.env.NODE_ENV === 'development') {
|
|
82
|
+
const { host, port } = getDevHost()
|
|
83
|
+
return `http://${host}:${port}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (process.env.VERCEL_ENV === 'production') {
|
|
87
|
+
return config.baseURL || `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.env.VERCEL_URL) {
|
|
91
|
+
return `https://${process.env.VERCEL_URL}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return config.baseURL
|
|
95
|
+
}
|
package/src/libs/toc.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import type { MarkdownHeading } from 'astro'
|
|
2
|
-
// import { getConfig } from './config'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Minimum configuration shape required by `generateToc`.
|
|
5
|
+
* The consuming project's full config object satisfies this interface as long as
|
|
6
|
+
* it contains a `toc` section with `min` and `max` depth values.
|
|
7
|
+
*/
|
|
8
|
+
export interface TocConfig {
|
|
9
|
+
toc: {
|
|
10
|
+
/** Minimum heading depth to include (e.g. `2` to start at `<h2>`). */
|
|
11
|
+
min: number
|
|
12
|
+
/** Maximum heading depth to include (e.g. `4` to stop at `<h4>`). */
|
|
13
|
+
max: number
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a nested tree of TOC entries from a flat list of Markdown headings.
|
|
19
|
+
*
|
|
20
|
+
* Headings outside the depth range defined in `config.toc` are excluded. When
|
|
21
|
+
* a heading skips one or more levels (e.g. an `<h4>` follows an `<h2>` with no
|
|
22
|
+
* `<h3>` in between) it is attached to the deepest available parent rather than
|
|
23
|
+
* being silently dropped.
|
|
24
|
+
*/
|
|
25
|
+
export function generateToc(allHeadings: MarkdownHeading[], config: TocConfig): TocEntry[] {
|
|
6
26
|
const headings = allHeadings.filter(
|
|
7
27
|
(heading) => heading.depth >= config.toc.min && heading.depth <= config.toc.max
|
|
8
28
|
)
|
|
@@ -29,14 +49,28 @@ export function generateToc(allHeadings: MarkdownHeading[], config: any) {
|
|
|
29
49
|
return toc
|
|
30
50
|
}
|
|
31
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Walks down the TOC tree to find the children array at the given relative
|
|
54
|
+
* `depth` below `entry`. If an intermediate level is missing (skipped heading),
|
|
55
|
+
* falls back to the deepest available children array so the heading is not lost.
|
|
56
|
+
*/
|
|
32
57
|
function getEntryChildrenAtDepth(entry: TocEntry, depth: number): TocEntry['children'] {
|
|
33
58
|
if (!entry) {
|
|
34
59
|
return []
|
|
35
60
|
}
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
if (depth === 1) {
|
|
63
|
+
return entry.children
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lastChild = entry.children[entry.children.length - 1]
|
|
67
|
+
|
|
68
|
+
if (!lastChild) {
|
|
69
|
+
// Heading skipped a level — attach to the current entry's children instead.
|
|
70
|
+
return entry.children
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return getEntryChildrenAtDepth(lastChild, depth - 1)
|
|
40
74
|
}
|
|
41
75
|
|
|
42
76
|
export interface TocEntry extends MarkdownHeading {
|