@chassis-ui/docs 0.1.10 → 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 +8 -8
- package/src/components/NavLink.astro +1 -1
- package/src/components/TableOfContents.astro +8 -7
- package/src/components/ThemeToggler.astro +16 -37
- package/src/components/shortcodes/Blockquote.astro +31 -0
- package/src/components/shortcodes/CSSOnly.astro +22 -0
- package/src/components/shortcodes/Callout.astro +6 -2
- package/src/components/shortcodes/Code.astro +289 -0
- package/src/components/shortcodes/DeprecatedIn.astro +18 -0
- package/src/components/shortcodes/Example.astro +132 -0
- package/src/components/shortcodes/Icon.astro +57 -0
- package/src/components/shortcodes/InFigma.astro +26 -0
- 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 +18 -21
- package/src/layouts/RedirectLayout.astro +1 -0
- package/src/layouts/SingleLayout.astro +3 -3
- package/src/layouts/footer/Footer.astro +5 -5
- package/src/layouts/footer/Scripts.astro +14 -7
- 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 +40 -42
- package/src/layouts/header/SearchDialog.astro +74 -0
- package/src/layouts/header/SearchTrigger.astro +41 -0
- package/src/layouts/header/Skippy.astro +1 -1
- 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 -61
- package/src/scss/_navbar.scss +19 -126
- 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 -81
- 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/_masthead.scss +0 -124
- package/src/scss/_settings.scss +0 -29
- package/src/scss/_syntax.scss +0 -160
- package/src/scss/_variables.scss +0 -78
package/src/libs/icon.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as htmlparser2 from 'htmlparser2'
|
|
2
|
+
|
|
3
|
+
const iconRegex = /<Icon\s+([^>]+)\/>/g
|
|
4
|
+
|
|
5
|
+
interface IconOptions {
|
|
6
|
+
name: string
|
|
7
|
+
class?: string
|
|
8
|
+
size: number
|
|
9
|
+
font: boolean
|
|
10
|
+
sprite: string
|
|
11
|
+
title?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Replaces `<Icon />` elements described using the `<Icon />` Astro component in HTML markup with the
|
|
16
|
+
* expected HTML content.
|
|
17
|
+
*
|
|
18
|
+
* This is needed because the `<Example />` shortcode renders its `code` prop as raw HTML via `set:html`
|
|
19
|
+
* rather than as MDX, so nested Astro components (like `<Icon />`, similar to `<Placeholder />`) are never
|
|
20
|
+
* actually invoked — they need to be expanded to their rendered markup ahead of time.
|
|
21
|
+
* @see src/components/shortcodes/Icon.astro
|
|
22
|
+
*/
|
|
23
|
+
export function replaceIconsInHtml(html: string) {
|
|
24
|
+
return html.replace(iconRegex, (match) => {
|
|
25
|
+
const document = htmlparser2.parseDocument(match, { xmlMode: true })
|
|
26
|
+
const iconElement = document.firstChild
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
document.children.length > 1 ||
|
|
30
|
+
!iconElement ||
|
|
31
|
+
iconElement.type !== htmlparser2.ElementType.Tag ||
|
|
32
|
+
iconElement.name !== 'Icon'
|
|
33
|
+
) {
|
|
34
|
+
throw new Error('Invalid icon element.')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return renderIconToString(getOptionsWithDefaults(iconElement.attribs))
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getOptionsWithDefaults(attribs: Record<string, string>): IconOptions {
|
|
42
|
+
if (!attribs.name) {
|
|
43
|
+
throw new Error('Icon element is missing the required "name" attribute.')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: attribs.name,
|
|
48
|
+
class: attribs.class,
|
|
49
|
+
size: attribs.size ? Number(attribs.size.replace(/[{}]/g, '')) : 24,
|
|
50
|
+
font: attribs.font !== undefined && attribs.font !== '{false}',
|
|
51
|
+
sprite: attribs.sprite || '/static/icons/chassis-icons.svg',
|
|
52
|
+
title: attribs.title
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderIconToString({ name, class: className, size, font, sprite, title }: IconOptions) {
|
|
57
|
+
const classes = ['icon', font ? `cx-${name}` : undefined, className].filter(Boolean).join(' ')
|
|
58
|
+
const ariaHidden = title ? '' : ' aria-hidden="true"'
|
|
59
|
+
|
|
60
|
+
if (font) {
|
|
61
|
+
const ariaLabel = title ? ` aria-label="${escapeHtml(title)}"` : ''
|
|
62
|
+
return `<span class="${classes}"${ariaHidden}${ariaLabel}></span>`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const titleTag = title ? `<title>${escapeHtml(title)}</title>` : ''
|
|
66
|
+
return `<svg class="${classes}" width="${size}" height="${size}"${ariaHidden}>${titleTag}<use href="${sprite}#${name}"></use></svg>`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function escapeHtml(value: string) {
|
|
70
|
+
return value
|
|
71
|
+
.replace(/&/g, '&')
|
|
72
|
+
.replace(/</g, '<')
|
|
73
|
+
.replace(/>/g, '>')
|
|
74
|
+
.replace(/"/g, '"')
|
|
75
|
+
}
|
package/src/libs/image.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs'
|
|
2
2
|
import sizeOf from 'image-size'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Reads an image file from disk and returns its pixel dimensions.
|
|
6
|
+
*
|
|
7
|
+
* Intended for build-time use only (e.g. generating `<meta og:image:width/height>`
|
|
8
|
+
* tags). Do not call at runtime in a server or edge context where filesystem
|
|
9
|
+
* access is unavailable.
|
|
10
|
+
*
|
|
11
|
+
* @param imagePath - Absolute path to the image file.
|
|
12
|
+
* @returns An object with `width` and `height` in pixels.
|
|
13
|
+
* @throws If the file cannot be read or its dimensions cannot be determined.
|
|
14
|
+
*/
|
|
4
15
|
export async function getStaticImageSize(imagePath: string) {
|
|
5
16
|
const buffer = await fs.readFile(imagePath)
|
|
6
|
-
const size =
|
|
17
|
+
const size = sizeOf(new Uint8Array(buffer))
|
|
7
18
|
|
|
8
19
|
if (!size?.height || !size?.width) {
|
|
9
20
|
throw new Error(`Failed to get size of static image at '${imagePath}'.`)
|
package/src/libs/layout.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import type { HTMLAttributes, HTMLTag } from 'astro/types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Identifies which top-level layout a page is using. Passed down through
|
|
5
|
+
* layouts and components to conditionally render layout-specific UI (e.g.
|
|
6
|
+
* sidebar toggle, TOC, search).
|
|
7
|
+
*
|
|
8
|
+
* `undefined` means a plain page with no special layout context.
|
|
9
|
+
*/
|
|
3
10
|
export type Layout = 'docs' | 'icons' | 'examples' | 'single' | 'blog' | undefined
|
|
4
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extends Astro's standard HTML element attributes with an open-ended set of
|
|
14
|
+
* `data-*` attributes so that arbitrary data attributes can be passed as
|
|
15
|
+
* override props without TypeScript errors.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const bodyProps: LayoutOverridesHTMLAttributes<'body'> = {
|
|
19
|
+
* class: 'docs-body',
|
|
20
|
+
* 'data-cx-spy': 'scroll',
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
5
23
|
export type LayoutOverridesHTMLAttributes<TTag extends HTMLTag> = HTMLAttributes<TTag> & {
|
|
6
24
|
[key in `data-${string}`]: string //eslint-disable-line no-unused-vars
|
|
7
25
|
}
|
|
@@ -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
|
+
}
|