diff --git a/packages/components/package.json b/packages/components/package.json index 48593512..9e72db73 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -57,21 +57,27 @@ }, "dependencies": { "@radix-ui/react-tooltip": "^1.1.4", + "@shikijs/transformers": "^3.21.0", "@sindresorhus/slugify": "^2.1.1", "clsx": "^2.1.1", "color": "^4.2.3", + "comlink": "^4.4.2", + "hast": "^1.0.0", "lodash": "^4.17.21", "lucide-react": "^0.453.0", "mermaid": "^11.12.2", + "shiki": "^3.17.0", "tailwind-merge": "^2.6.0" }, "devDependencies": { + "@shikijs/types": "^3.18.0", "@storybook/addon-docs": "^10.1.10", "@storybook/react-vite": "^10.1.10", "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", "@types/color": "^4.2.0", + "@types/hast": "^3.0.4", "@types/lodash": "4.17.23", "@types/node": "^22.10.1", "@types/react": "^18.3.12", diff --git a/packages/components/src/code.css b/packages/components/src/code.css new file mode 100644 index 00000000..5e2b1e40 --- /dev/null +++ b/packages/components/src/code.css @@ -0,0 +1,398 @@ +@import "./twoslash.css"; + +.code-block { + --fade-width: 0px; + --code-padding-right: 48px; +} + +:not(.code-block) > [data-component-part="code-block-root"] { + --code-padding-right: 48px; +} + +.code-block:has([data-floating-buttons] > :nth-child(1):last-child) { + --fade-width: 99px; + --code-padding-right: 99px; +} + +.code-block:has([data-floating-buttons] > :nth-child(2):last-child) { + --fade-width: 131px; + --code-padding-right: 131px; +} + +.code-block:has([data-floating-buttons] > :nth-child(3):last-child) { + --fade-width: 163px; + --code-padding-right: 163px; +} + +.code-block:has([data-component-part="code-block-root"].code-block-wrap) { + --code-padding-right: 0px !important; +} + +[data-fade-overlay] { + @apply absolute top-0 right-0 h-[44px] pointer-events-none z-[1] rounded-tr-2xl rounded-br-xl; + + width: var(--fade-width, 0px); + background: linear-gradient( + to right, + transparent 0px, + color-mix( + in srgb, + var(--fade-color-light, var(--background-light)) 20%, + transparent + ) + 10px, + color-mix( + in srgb, + var(--fade-color-light, var(--background-light)) 50%, + transparent + ) + 25px, + color-mix( + in srgb, + var(--fade-color-light, var(--background-light)) 80%, + transparent + ) + 35px, + var(--fade-color-light, var(--background-light)) 45px + ); +} + +html.dark [data-fade-overlay] { + background: linear-gradient( + to right, + transparent 0px, + color-mix( + in srgb, + var(--fade-color-dark, var(--background-dark)) 20%, + transparent + ) + 10px, + color-mix( + in srgb, + var(--fade-color-dark, var(--background-dark)) 50%, + transparent + ) + 25px, + color-mix( + in srgb, + var(--fade-color-dark, var(--background-dark)) 80%, + transparent + ) + 35px, + var(--fade-color-dark, var(--background-dark)) 45px + ); +} + +:has([data-floating-buttons]) + > [data-component-part="code-block-root"] + pre + > code { + padding-right: var(--code-padding-right, 0px) !important; +} + +:has([data-component-part="code-block-header"]) + > [data-component-part="code-block-root"] + pre + > code { + padding-right: var(--code-padding-right, 48px) !important; +} + +[data-component-part="code-block-root"] pre > code > span.line-highlight, +[data-component-part="code-block-root"] pre > code > span.line-diff { + min-width: calc(100% + var(--code-padding-right, 0px)); +} + +html.dark .shiki, +html.dark .shiki span { + position: relative; + color: var(--shiki-dark) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; +} + +pre.shiki { + background-color: transparent !important; +} + +html.dark .shiki .line-highlight span { + background-color: transparent !important; +} + +html.dark .code-block-background { + background-color: var(--shiki-dark-bg) !important; +} + +.code-block:has(.line-diff) [data-fade-overlay] { + background: transparent !important; +} + +html.dark, +html:not(.dark) .codeblock-dark { + pre.shiki > code { + position: relative; + z-index: 1; + display: block; + width: max-content; + min-width: 100%; + } + + pre[class^="language-diff-"] { + @apply flex px-9; + } + + pre[class^="language-diff-"] > code { + @apply flex-none min-w-full; + } +} + +html:not(.dark) .codeblock-light { + pre.shiki > code { + position: relative; + z-index: 1; + display: block; + width: max-content; + min-width: 100%; + } + + pre[class^="language-diff-"] { + @apply flex px-9; + } + + pre[class^="language-diff-"] > code { + @apply flex-none min-w-full; + } +} + +/* Lines of code outside code blocks. Generated when you use single ticks: `example` */ +:not(pre) > code { + @apply break-words rounded-md px-2 py-0.5 text-gray-600 dark:text-gray-200 dark:border-gray-800 bg-gray-100/50 dark:bg-white/5; +} + +:not(pre) > code:before, +:not(pre) > code:after { + content: none !important; +} + +/* text wrapping for code blocks */ +.code-block-wrap pre.shiki > code { + width: 100% !important; + padding-right: 0 !important; + > .line-highlight { + padding-right: 3rem !important; + } +} +.code-block-wrap pre.shiki .line span { + white-space: pre-wrap !important; + word-break: break-word; +} + +/* line focus */ +.has-focused pre.shiki > code .line { + filter: blur(0.1rem); + transition: + filter 0.35s, + opacity 0.35s; + + &:hover { + filter: blur(0); + } +} + +.has-focused pre.shiki:hover > code .line { + filter: blur(0); +} + +.has-focused pre.shiki > code .line-focus { + filter: blur(0); +} + +/* line highlighting (without line numbers) */ +[data-component-part="code-block-root"]:not(.has-line-numbers) { + .line-highlight { + background: color-mix( + in srgb, + var(--color-primary-light) 20%, + transparent + ) !important; + width: 100%; + display: inline-block; + position: relative; + z-index: 0; + + &::before, + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + width: 1rem; + background: color-mix( + in srgb, + var(--color-primary-light) 20%, + transparent + ) !important; + } + + &::before { + left: -1rem; + border-left: 1px solid var(--color-primary-light); + } + + &::after { + right: -1rem; + } + } + + .line-diff { + width: 100%; + display: inline-block; + position: relative; + z-index: 0; + + &::before { + position: absolute; + top: 0; + bottom: 0; + left: -1rem; + width: 1rem; + display: flex; + align-items: center; + justify-content: center; + } + + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + right: -1rem; + width: 1rem; + } + + &.line-add { + background: rgb(34 197 94 / 0.15) !important; + + &::before { + content: "+"; + background: rgb(34 197 94 / 0.15) !important; + border-left: 1px solid rgb(34 197 94 / 0.8); + color: rgb(34 197 94); + } + + &::after { + background: rgb(34 197 94 / 0.15) !important; + } + } + + &.line-remove { + background: rgb(239 68 68 / 0.15) !important; + + &::before { + content: "-"; + background: rgb(239 68 68 / 0.15) !important; + border-left: 1px solid rgb(239 68 68 / 0.8); + color: rgb(239 68 68); + } + + &::after { + background: rgb(239 68 68 / 0.15) !important; + } + } + } +} + +/* line numbers */ +.has-line-numbers pre.shiki > code { + counter-reset: step; + counter-increment: step 0; +} + +.has-line-numbers pre.shiki > code > .line::before { + content: counter(step); + counter-increment: step; + width: 0.6rem; + margin-right: 1.1rem; + display: inline-block; + text-align: right; + user-select: none; + .dark & { + color: var(--shiki-dark) !important; + } + :not(.dark) & { + color: var(--shiki-light) !important; + opacity: 0.4 !important; + } +} + +/* line numbers + highlighting */ +.has-line-numbers .line-highlight { + width: 100%; + display: inline-block; + position: relative; + + &::before { + display: none; + } + + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -1rem; + right: -1rem; + background: color-mix( + in srgb, + var(--color-primary-light) 20%, + transparent + ) !important; + border-left: 1px solid var(--color-primary-light); + z-index: -1; + } +} + +.has-line-numbers .line-diff { + width: 100%; + display: inline-block; + position: relative; + + &.line-add::before { + content: "+" !important; + } + + &.line-remove::before { + content: "-" !important; + } + + &.line-add::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -1rem; + right: -1rem; + background: rgb(34 197 94 / 0.15) !important; + border-left: 1px solid rgb(34 197 94 / 0.8); + z-index: -1; + } + + &.line-remove::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -1rem; + right: -1rem; + background: rgb(239 68 68 / 0.15) !important; + border-left: 1px solid rgb(239 68 68 / 0.8); + z-index: -1; + } +} + +.nct-inline-flex { + z-index: -9999; +} + +code[data-symbols="true"] { + @apply font-[inherit]; +} diff --git a/packages/components/src/common/get-code-styling.ts b/packages/components/src/common/get-code-styling.ts new file mode 100644 index 00000000..78f4e0b3 --- /dev/null +++ b/packages/components/src/common/get-code-styling.ts @@ -0,0 +1,48 @@ +import { + DEFAULT_DARK_THEME, + DEFAULT_LIGHT_THEME, + type ShikiThemeType, +} from "@/utils/shiki/constants"; +import type { CodeStyling } from "@/validation"; + +const codeStylingToThemeOrThemes = ( + codeStylingFromConfig: CodeStyling +): { themes: { light: ShikiThemeType; dark: ShikiThemeType } } => { + if ( + typeof codeStylingFromConfig === "object" && + "theme" in codeStylingFromConfig && + typeof codeStylingFromConfig.theme === "object" + ) { + return { + themes: codeStylingFromConfig.theme, + }; + } + if ( + typeof codeStylingFromConfig === "object" && + "theme" in codeStylingFromConfig && + typeof codeStylingFromConfig.theme === "string" + ) { + return { + themes: { + light: codeStylingFromConfig.theme, + dark: codeStylingFromConfig.theme, + }, + }; + } + if (codeStylingFromConfig === "dark") { + return { + themes: { + light: DEFAULT_DARK_THEME, + dark: DEFAULT_DARK_THEME, + }, + }; + } + return { + themes: { + light: DEFAULT_LIGHT_THEME, + dark: DEFAULT_DARK_THEME, + }, + }; +}; + +export { codeStylingToThemeOrThemes }; diff --git a/packages/components/src/components/code-block/base-code-block.tsx b/packages/components/src/components/code-block/base-code-block.tsx new file mode 100644 index 00000000..6aa50dbf --- /dev/null +++ b/packages/components/src/components/code-block/base-code-block.tsx @@ -0,0 +1,211 @@ +import type React from "react"; + +import { + DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT, + SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT, + SMALL_EXPANDABLE_NUMBER_OF_LINES, + useExpandable, +} from "@/hooks/use-expandable"; +import { useGetShikiHighlightedHtml } from "@/hooks/use-get-shiki-highlighted-html"; +import { cn } from "@/utils/cn"; +import { getShikiBackgroundColors } from "@/utils/shiki/get-shiki-background-colors"; +import { getCodeString, useCalculateCodeLines } from "@/utils/shiki/lib"; + +import type { CodeBlockProps } from "./code-block"; +import { CodeFooter } from "./code-footer"; + +interface BaseCodeBlockProps extends CodeBlockProps { + isParentCodeGroup?: boolean; + shouldHighlight?: boolean; + // pass isLivePreview to forceExtract to force code string theme changes + forceExtract?: boolean; +} + +const BaseCodeBlock = ({ + expandable = false, + shouldHighlight = true, + children, + isParentCodeGroup, + isSmallText, + numberOfLines, + forceExtract = false, + codeBlockTheme = "system", + codeBlockThemeObject = "system", + ...props + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO +}: BaseCodeBlockProps) => { + const { focus, highlight, lang, language, className, wrap, lines } = props; + + const codeString = getCodeString(children, props.className, forceExtract); + + const opts: { + highlightedLines?: number[]; + focusedLines?: number[]; + } = {}; + + let focusLines: number[] = []; + let highlightLines: number[] = []; + + if (focus) { + try { + const parsed = JSON.parse(focus); + focusLines = Array.isArray(parsed) ? parsed : []; + } catch { + focusLines = []; + } + } + + if (highlight) { + try { + const parsed = JSON.parse(highlight); + highlightLines = Array.isArray(parsed) ? parsed : []; + } catch { + highlightLines = []; + } + } + + if (highlightLines.length > 0) { + opts.highlightedLines = highlightLines; + } + if (focusLines.length > 0) { + opts.focusedLines = focusLines; + } + + const selectedLang = lang || language; + + const html = useGetShikiHighlightedHtml( + selectedLang + ? { + codeString, + codeBlockTheme: codeBlockThemeObject, + language: shouldHighlight ? selectedLang : "text", + opts, + } + : { + codeString, + codeBlockTheme: codeBlockThemeObject, + className: shouldHighlight ? className : "lang-text", + opts, + } + ); + + numberOfLines = useCalculateCodeLines(html, numberOfLines); + const { isExpanded, calculatedHeight, contentRef, toggleExpanded } = + useExpandable(expandable, numberOfLines); + const shikiBackgroundColors = getShikiBackgroundColors( + codeBlockTheme, + html, + children + ); + + const isFocusEnabled = focusLines.length > 0; + const isHighlightEnabled = highlightLines.length > 0; + const getHeight = () => { + if (expandable) { + if (isExpanded) { + return `${calculatedHeight}px`; + } + if (numberOfLines && numberOfLines < SMALL_EXPANDABLE_NUMBER_OF_LINES) { + return `${SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT}px`; + } + return `${DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT}px`; + } + if (isParentCodeGroup) { + return "100%"; + } + return "auto"; + }; + const height = getHeight(); + + return ( + <> +
+ {html ? ( +
+ ) : ( +
+ {children} +
+ )} +
+ {!(wrap || isHighlightEnabled || expandable) && + focusLines.length === 0 && ( +
+ )} + {expandable && ( + + )} + + ); +}; + +export { type BaseCodeBlockProps, BaseCodeBlock }; diff --git a/packages/components/src/components/code-block/code-block.stories.tsx b/packages/components/src/components/code-block/code-block.stories.tsx new file mode 100644 index 00000000..7f98bc3b --- /dev/null +++ b/packages/components/src/components/code-block/code-block.stories.tsx @@ -0,0 +1,466 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { CodeBlock } from "./code-block"; +import { + complexDiffCode, + diffCodeWithRemove, + longExpandableCode, + longSingleLineCode, + mediumCode, + simpleDiffCode, + singleLineCode, +} from "./story-utils"; + +const meta: Meta = { + title: "Components/CodeBlock", + component: CodeBlock, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const SingleLineCode: Story = { + render: () => {singleLineCode}, +}; + +export const LongSingleLineCode: Story = { + render: () => {longSingleLineCode}, +}; + +export const SystemTheme: Story = { + render: (_, { globals }) => ( + + {mediumCode} + + ), +}; + +export const DarkTheme: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithShikiTheme: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithLightDarkShikiThemes: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithFilename: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithIcon: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithSpacesInFilename: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithLineNumbers: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithLineNumbersDark: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithWrap: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithWrapAndLineNumbers: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WrapMultiLine: Story = { + render: () => {longSingleLineCode}, +}; + +export const WithHighlight: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithHighlightAndLineNumbers: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const HighlightRange: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const HighlightMixed: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const HighlightWithShikiTheme: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithFocus: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const WithFocusAndLineNumbers: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const FocusRange: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const FocusMixed: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const HighlightAndFocus: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const HighlightAndFocusAndLineNumbers: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const HighlightFocusOverlap: Story = { + render: () => ( + + {mediumCode} + + ), +}; + +export const Expandable: Story = { + render: () => ( + + {longExpandableCode} + + ), +}; + +export const ExpandableAndLineNumbers: Story = { + render: () => ( + + {longExpandableCode} + + ), +}; + +export const ExpandableWithHighlight: Story = { + render: () => ( + + {longExpandableCode} + + ), +}; + +export const ExpandableWithFocus: Story = { + render: () => ( + + {longExpandableCode} + + ), +}; + +export const ExpandableAllFeatures: Story = { + render: () => ( + + {longExpandableCode} + + ), +}; + +export const EmptyCode: Story = { + render: () => ( + + {""} + + ), +}; + +export const SpecialCharacters: Story = { + render: () => ( + + {`// Special characters test +const symbols = "!@#$%^&*()_+-=[]{}|;':",./<>?"; +const unicode = "Hello 世界 🌍 café naïve résumé"; +const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/;`} + + ), +}; + +export const DiffBasic: Story = { + render: () => ( + + {simpleDiffCode} + + ), +}; + +export const DiffWithShikiTheme: Story = { + render: () => ( + + {simpleDiffCode} + + ), +}; + +export const DiffWithLineNumbers: Story = { + render: () => ( + + {simpleDiffCode} + + ), +}; + +export const DiffFunction: Story = { + render: () => ( + + {diffCodeWithRemove} + + ), +}; + +export const DiffFunctionWithLineNumbers: Story = { + render: () => ( + + {diffCodeWithRemove} + + ), +}; + +export const DiffReactExample: Story = { + render: () => ( + + {complexDiffCode} + + ), +}; + +export const DiffExpandable: Story = { + render: () => ( + + {complexDiffCode} + + ), +}; + +export const WithCustomClassName: Story = { + render: () => ( + + {mediumCode} + + ), +}; diff --git a/packages/components/src/components/code-block/code-block.tsx b/packages/components/src/components/code-block/code-block.tsx new file mode 100644 index 00000000..00b87e21 --- /dev/null +++ b/packages/components/src/components/code-block/code-block.tsx @@ -0,0 +1,153 @@ +import type { ReactNode, RefObject } from "react"; + +import { Classes } from "@/lib/local/selectors"; +import { cn } from "@/utils/cn"; +import type { CopyToClipboardResult } from "@/utils/copy-to-clipboard"; +import { getNodeText } from "@/utils/get-node-text"; +import type { CodeStyling } from "@/validation"; + +import { BaseCodeBlock } from "./base-code-block"; +import { CodeHeader } from "./code-header"; +import { CopyToClipboardButton } from "./copy-button"; + +type CodeBlockProps = { + language?: string; + filename?: string; + icon?: string; + lang?: string; + className?: string; + /** + * Whether to show line numbers. + */ + lines?: boolean; + /** + * Whether to wrap the code block. + */ + wrap?: boolean; + /** + * Whether to expand the code block. + */ + expandable?: boolean; + /** + * The lines to highlight. it's a stringified array of numbers. + * Example: "[1,3,4,5]" + */ + highlight?: string; + /** + * The lines to focus on. it's a stringified array of numbers. + * Example: "[1,3,4,5]" + */ + focus?: string; + /** + * Internal prop to set the number of lines in the code block. + * Users should not set this prop. + */ + numberOfLines?: number; + /** + * Prop to hide the ask ai button + */ + hideAskAiButton?: boolean; + /** + * Prop to set the small text size + */ + isSmallText?: boolean; + /** + * Pass in from CodeSnippetFeedbackProvider + */ + feedbackModalOpen?: boolean; + /** + * Pass in from CodeSnippetFeedbackProvider + */ + anchorRef?: RefObject; + /** + * Prop to set the code block theme (code block UI theme) + */ + codeBlockTheme?: "dark" | "system"; + /** + * Prop to set the code block theme object (syntax highlighting theme) + */ + codeBlockThemeObject?: CodeStyling; + /** + * Pass in AskAiCodeBlockButton component + */ + askAiButton?: ReactNode; + /** + * Pass in CodeSnippetFeedbackButton component + */ + feedbackButton?: ReactNode; + /** + * The callback function when a user clicks on the copied to clipboard button + */ + onCopied?: (result: CopyToClipboardResult, textToCopy?: string) => void; + children?: ReactNode; +}; + +const CodeBlock = function CodeBlock(params: CodeBlockProps) { + const { + filename, + onCopied, + children, + className, + icon, + isSmallText, + hideAskAiButton, + feedbackModalOpen, + anchorRef, + codeBlockTheme = "system", + codeBlockThemeObject, + askAiButton, + feedbackButton, + } = params; + + const codeString = getNodeText(children); + const hasGrayBackgroundContainer = !!filename || !!icon; + + return ( +
+ {hasGrayBackgroundContainer ? ( + + {feedbackButton && feedbackButton} + + {askAiButton && askAiButton} + + ) : ( +
+ {feedbackButton && feedbackButton} + + {askAiButton && askAiButton} +
+ )} + +
+ ); +}; + +export { type CodeBlockProps, CodeBlock }; diff --git a/packages/components/src/components/code-block/code-footer.tsx b/packages/components/src/components/code-block/code-footer.tsx new file mode 100644 index 00000000..3dc43ac0 --- /dev/null +++ b/packages/components/src/components/code-block/code-footer.tsx @@ -0,0 +1,38 @@ +import { EllipsisIcon } from "lucide-react"; + +type CodeFooterProps = { + numberOfLines: number | undefined; + isExpanded: boolean; + toggleExpanded: () => void; +}; + +const CodeFooter = ({ + numberOfLines, + isExpanded, + toggleExpanded, +}: CodeFooterProps) => { + if (!numberOfLines) { + return null; + } + + return ( +
+ +
+ ); +}; + +export { type CodeFooterProps, CodeFooter }; diff --git a/packages/components/src/components/code-block/code-header.tsx b/packages/components/src/components/code-block/code-header.tsx new file mode 100644 index 00000000..cc3e6941 --- /dev/null +++ b/packages/components/src/components/code-block/code-header.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from "react"; + +import { Icon as ComponentIcon } from "@/components/icon"; +import { Classes } from "@/lib/local/selectors"; +import { cn } from "@/utils/cn"; + +type CodeHeaderProps = { + filename?: string; + icon?: string; + codeBlockTheme?: "dark" | "system"; + children?: ReactNode; +}; + +const CodeHeader = ({ + filename, + icon, + codeBlockTheme = "system", + children, +}: CodeHeaderProps) => { + return ( +
+
+ {icon && ( + + )} + {filename} +
+
+ {children} +
+
+ ); +}; + +export { type CodeHeaderProps, CodeHeader }; diff --git a/packages/components/src/components/code-block/copy-button.tsx b/packages/components/src/components/code-block/copy-button.tsx new file mode 100644 index 00000000..9f71605e --- /dev/null +++ b/packages/components/src/components/code-block/copy-button.tsx @@ -0,0 +1,99 @@ +import { type ComponentPropsWithoutRef, useEffect, useState } from "react"; +import { ActiveCopyButtonIcon, CopyButtonIcon } from "@/icons"; +import { cn } from "@/utils/cn"; +import { + type CopyToClipboardResult, + copyToClipboard, +} from "@/utils/copy-to-clipboard"; + +const DEFAULT_COPY_BUTTON_ARIA_LABEL = "Copy the contents from the code block"; +const DEFAULT_TOOLTIP_COPY_TEXT = "Copy"; +const DEFAULT_TOOLTIP_COPIED_TEXT = "Copied!"; + +type CopyToClipboardButtonProps = { + textToCopy: string; + onCopied?: (result: CopyToClipboardResult, textToCopy?: string) => void; + className?: string; + showTooltip?: boolean; + codeBlockTheme?: "system" | "dark"; + copyButtonAriaLabel?: string; + tooltipCopyText?: string; + tooltipCopiedText?: string; +}; + +const CopyToClipboardButton = ({ + textToCopy, + onCopied, + className, + showTooltip = true, + codeBlockTheme, + copyButtonAriaLabel = DEFAULT_COPY_BUTTON_ARIA_LABEL, + tooltipCopyText = DEFAULT_TOOLTIP_COPY_TEXT, + tooltipCopiedText = DEFAULT_TOOLTIP_COPIED_TEXT, + ...buttonProps +}: CopyToClipboardButtonProps & ComponentPropsWithoutRef<"button">) => { + const trimmedTextToCopy = textToCopy.trim(); + const [isCopiedActive, setIsCopiedActive] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + + useEffect(() => { + // Hide copy button if the browser does not support it + if (typeof window !== "undefined" && !navigator.clipboard) { + console.warn( + "The browser's Clipboard API is unavailable. The Clipboard API is only available on HTTPS." + ); + setIsDisabled(true); + } else { + setIsDisabled(false); + } + }, []); + + // Hide copy button if you would copy an empty string + if (!trimmedTextToCopy || isDisabled) { + return null; + } + + const onCopy = async () => { + const result = await copyToClipboard(trimmedTextToCopy); + if (onCopied) { + onCopied(result, trimmedTextToCopy); + } + if (result === "success") { + setIsCopiedActive(true); + setTimeout(() => { + setIsCopiedActive(false); + }, 2000); + } + }; + + return ( +
+ + {showTooltip && ( +
+ {isCopiedActive ? tooltipCopiedText : tooltipCopyText} +
+ )} +
+ ); +}; + +export { type CopyToClipboardButtonProps, CopyToClipboardButton }; diff --git a/packages/components/src/components/code-block/index.tsx b/packages/components/src/components/code-block/index.tsx new file mode 100644 index 00000000..a9e6c6a4 --- /dev/null +++ b/packages/components/src/components/code-block/index.tsx @@ -0,0 +1,3 @@ +// biome-ignore lint/performance/noBarrelFile: TODO +export { BaseCodeBlock, type BaseCodeBlockProps } from "./base-code-block"; +export { CodeBlock, type CodeBlockProps } from "./code-block"; diff --git a/packages/components/src/components/code-block/story-utils.ts b/packages/components/src/components/code-block/story-utils.ts new file mode 100644 index 00000000..597356eb --- /dev/null +++ b/packages/components/src/components/code-block/story-utils.ts @@ -0,0 +1,143 @@ +const mediumCode = `class HelloWorld { + private String message; + + public HelloWorld(String message) { + this.message = message; + } + + public void greet() { + System.out.println("Hello, World!"); + System.out.println(this.message); + } + + public static void main(String[] args) { + HelloWorld hw = new HelloWorld("Welcome to Java!"); + hw.greet(); + } +}`; + +const longExpandableCode = `from datetime import datetime, timedelta +from typing import Dict, List, Optional +from dataclasses import dataclass + +@dataclass +class Book: + title: str + author: str + isbn: str + checked_out: bool = False + due_date: Optional[datetime] = None + +class Library: + def __init__(self): + self.books: Dict[str, Book] = {} + self.checkouts: Dict[str, List[str]] = {} # patron -> list of ISBNs + + def add_book(self, book: Book) -> None: + if book.isbn in self.books: + raise ValueError(f"Book with ISBN {book.isbn} already exists") + self.books[book.isbn] = book + + def checkout_book(self, isbn: str, patron: str, days: int = 14) -> None: + if patron not in self.checkouts: + self.checkouts[patron] = [] + + book = self.books.get(isbn) + if not book: + raise ValueError("Book not found") + + if book.checked_out: + raise ValueError("Book is already checked out") + + if len(self.checkouts[patron]) >= 3: + raise ValueError("Patron has reached checkout limit") + + book.checked_out = True + book.due_date = datetime.now() + timedelta(days=days) + self.checkouts[patron].append(isbn) + + def return_book(self, isbn: str) -> float: + book = self.books.get(isbn) + if not book or not book.checked_out: + raise ValueError("Book not found or not checked out") + + late_fee = 0.0 + if datetime.now() > book.due_date: + days_late = (datetime.now() - book.due_date).days + late_fee = days_late * 0.50 + + book.checked_out = False + book.due_date = None + + # Remove from patron's checkouts + for patron, books in self.checkouts.items(): + if isbn in books: + books.remove(isbn) + break + + return late_fee`; + +const longSingleLineCode = + "To denote a `word` or `phrase` as code, enclose it in backticks (`). The backtick ` is a typographical mark used mainly in computing. It is also known as backquote, grave, or grave accent. This line is intentionally very long to test wrapping behavior and horizontal scrolling."; + +const singleLineCode = + "To denote a `word` or `phrase` as code, enclose it in backticks (`)"; + +const diffCodeWithRemove = `function calculateTotal(items) { + let total = 0; + for (let i = 0; i < items.length; i++) { // [!code --] + total += items[i].price; // [!code --] + for (const item of items) { // [!code ++] + total += item.price; // [!code ++] + total += item.tax; // [!code ++] + } + return total; +} + +function processOrder(order) { + const total = calculateTotal(order.items); + const discount = calculateDiscount(order); // [!code ++] + order.total = total - discount; // [!code ++] + order.total = total; // [!code --] + return order; +}`; + +const simpleDiffCode = `console.log("Hello World"); +console.log("Old message"); // [!code --] +console.log("New message"); // [!code ++] +console.log("End");`; + +const complexDiffCode = `import React from 'react'; +import { useState } from 'react'; // [!code --] +import { useState, useEffect } from 'react'; // [!code ++] + +function MyComponent() { + const [count, setCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); // [!code ++] + + useEffect(() => { // [!code ++] + setIsLoading(true); // [!code ++] + // Simulate async operation // [!code ++] + setTimeout(() => setIsLoading(false), 1000); // [!code ++] + }, []); // [!code ++] + + return ( +
+

Count: {count}

// [!code --] +

{isLoading ? 'Loading...' : \`Count: \${count}\`}

// [!code ++] + +
+ ); +}`; + +export { + mediumCode, + longExpandableCode, + longSingleLineCode, + singleLineCode, + diffCodeWithRemove, + simpleDiffCode, + complexDiffCode, +}; diff --git a/packages/components/src/components/index.ts b/packages/components/src/components/index.ts index c0fbbfba..1ba93437 100644 --- a/packages/components/src/components/index.ts +++ b/packages/components/src/components/index.ts @@ -3,6 +3,7 @@ export * from "./accordion"; export * from "./badge"; export * from "./callout"; export * from "./card"; +export * from "./code-block"; export * from "./code-group"; export * from "./color"; export * from "./columns"; diff --git a/packages/components/src/constants/index.ts b/packages/components/src/constants/index.ts index 890ef254..07221844 100644 --- a/packages/components/src/constants/index.ts +++ b/packages/components/src/constants/index.ts @@ -1,4 +1,5 @@ const MINTLIFY_ICONS_CDN_URL = "https://d3gk2c5xim1je2.cloudfront.net"; const MAX_DEFAULT_VALUE_LENGTH = 50; +const MAX_PREVIEW_BYTES = 50_000; // 0.05 mb / 50 kb -export { MINTLIFY_ICONS_CDN_URL, MAX_DEFAULT_VALUE_LENGTH }; +export { MINTLIFY_ICONS_CDN_URL, MAX_DEFAULT_VALUE_LENGTH, MAX_PREVIEW_BYTES }; diff --git a/packages/components/src/hooks/use-expandable.ts b/packages/components/src/hooks/use-expandable.ts new file mode 100644 index 00000000..a22aa6c2 --- /dev/null +++ b/packages/components/src/hooks/use-expandable.ts @@ -0,0 +1,47 @@ +import { useEffect, useRef, useState } from "react"; + +const SMALL_EXPANDABLE_NUMBER_OF_LINES = 7; +const SMALL_EXPANDED_HEIGHT_OFFSET = 30; +const EXPANDED_HEIGHT_OFFSET = 40; +const SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT = 45; +const DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT = 190; + +const useExpandable = (enable: boolean, numberOfLines?: number) => { + const [isExpanded, setIsExpanded] = useState(false); + const [calculatedHeight, setCalculatedHeight] = useState(0); + const contentRef = useRef(null); + + useEffect(() => { + let rafId: number | undefined; + if (contentRef.current != null && enable) { + rafId = requestAnimationFrame(() => { + if (contentRef.current != null) { + setCalculatedHeight( + contentRef.current.scrollHeight + + (numberOfLines && numberOfLines < SMALL_EXPANDABLE_NUMBER_OF_LINES + ? SMALL_EXPANDED_HEIGHT_OFFSET + : EXPANDED_HEIGHT_OFFSET) + ); + } + }); + } + return () => { + if (rafId !== undefined) { + cancelAnimationFrame(rafId); + } + }; + }, [enable, numberOfLines]); + + const toggleExpanded = () => setIsExpanded((prev) => !prev); + + return { isExpanded, calculatedHeight, contentRef, toggleExpanded }; +}; + +export { + SMALL_EXPANDABLE_NUMBER_OF_LINES, + SMALL_EXPANDED_HEIGHT_OFFSET, + EXPANDED_HEIGHT_OFFSET, + SMALL_EXPANDABLE_CODE_BLOCK_HEIGHT, + DEFAULT_EXPANDABLE_CODE_BLOCK_HEIGHT, + useExpandable, +}; diff --git a/packages/components/src/hooks/use-get-shiki-highlighted-html.ts b/packages/components/src/hooks/use-get-shiki-highlighted-html.ts new file mode 100644 index 00000000..99cae206 --- /dev/null +++ b/packages/components/src/hooks/use-get-shiki-highlighted-html.ts @@ -0,0 +1,56 @@ +import { useState } from "react"; + +import { useIsomorphicLayoutEffect } from "@/hooks/use-isomorphic-layout-effect"; +import { + getShikiHighlightedHtml, + type ShikiHighlightedHtmlArgs, +} from "@/utils/shiki"; + +const useGetShikiHighlightedHtml = ( + props: ShikiHighlightedHtmlArgs +): string | undefined => { + const htmlOrPromise = getShikiHighlightedHtml(props); + + const [html, setHtml] = useState( + htmlOrPromise instanceof Promise ? undefined : htmlOrPromise + ); + + useIsomorphicLayoutEffect(() => { + if (!(htmlOrPromise instanceof Promise)) { + setHtml(htmlOrPromise); + return; + } + + let cancelled = false; + + async function getHtml() { + if (!(htmlOrPromise instanceof Promise)) { + setHtml(htmlOrPromise); + return; + } + + try { + const result = await htmlOrPromise; + if (!cancelled) { + setHtml(result); + } + } catch (err) { + console.error(err); + if (!cancelled) { + setHtml(undefined); + } + } + } + + // biome-ignore lint/complexity/noVoid: TODO + void getHtml(); + + return () => { + cancelled = true; + }; + }, [props, htmlOrPromise]); + + return html; +}; + +export { useGetShikiHighlightedHtml }; diff --git a/packages/components/src/hooks/use-isomorphic-layout-effect.ts b/packages/components/src/hooks/use-isomorphic-layout-effect.ts new file mode 100644 index 00000000..9bd32554 --- /dev/null +++ b/packages/components/src/hooks/use-isomorphic-layout-effect.ts @@ -0,0 +1,6 @@ +import { useEffect, useLayoutEffect } from "react"; + +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect; + +export { useIsomorphicLayoutEffect }; diff --git a/packages/components/src/icons/index.tsx b/packages/components/src/icons/index.tsx index 9a0f540b..21beef74 100644 --- a/packages/components/src/icons/index.tsx +++ b/packages/components/src/icons/index.tsx @@ -269,7 +269,72 @@ const LinkIcon = (props: SVGProps) => { ); }; +const ActiveCopyButtonIcon = ({ + codeBlockTheme = "system", +}: { + codeBlockTheme?: "system" | "dark"; +}) => { + return ( + + Copied + + + ); +}; + +const CopyButtonIcon = ({ + codeBlockTheme = "system", +}: { + codeBlockTheme?: "system" | "dark"; +}) => { + return ( + + Copy + + + + ); +}; + export { + ActiveCopyButtonIcon, + CopyButtonIcon, LinkIcon, InfoIcon, Folder2OpenIcon, diff --git a/packages/components/src/mdx/index.ts b/packages/components/src/mdx/index.ts new file mode 100644 index 00000000..da6d8bee --- /dev/null +++ b/packages/components/src/mdx/index.ts @@ -0,0 +1,66 @@ +const SHIKI_THEMES = [ + "andromeeda", + "aurora-x", + "ayu-dark", + "catppuccin-frappe", + "catppuccin-latte", + "catppuccin-macchiato", + "catppuccin-mocha", + "dark-plus", + "dracula", + "dracula-soft", + "everforest-dark", + "everforest-light", + "github-dark", + "github-dark-default", + "github-dark-dimmed", + "github-dark-high-contrast", + "github-light", + "github-light-default", + "github-light-high-contrast", + "gruvbox-dark-hard", + "gruvbox-dark-medium", + "gruvbox-dark-soft", + "gruvbox-light-hard", + "gruvbox-light-medium", + "gruvbox-light-soft", + "houston", + "kanagawa-dragon", + "kanagawa-lotus", + "kanagawa-wave", + "laserwave", + "light-plus", + "material-theme", + "material-theme-darker", + "material-theme-lighter", + "material-theme-ocean", + "material-theme-palenight", + "min-dark", + "min-light", + "monokai", + "night-owl", + "nord", + "one-dark-pro", + "one-light", + "plastic", + "poimandres", + "red", + "rose-pine", + "rose-pine-dawn", + "rose-pine-moon", + "slack-dark", + "slack-ochin", + "snazzy-light", + "solarized-dark", + "solarized-light", + "synthwave-84", + "tokyo-night", + "vesper", + "vitesse-black", + "vitesse-dark", + "vitesse-light", + + "css-variables", // for users who want to use custom CSS to style their code blocks +] as const; + +export { SHIKI_THEMES }; diff --git a/packages/components/src/plugins/scrollbar.ts b/packages/components/src/plugins/scrollbar.ts new file mode 100644 index 00000000..0889b06e --- /dev/null +++ b/packages/components/src/plugins/scrollbar.ts @@ -0,0 +1,86 @@ +import plugin from "tailwindcss/plugin"; + +export default plugin(({ addUtilities }) => { + const scrollbarBase = { + "scrollbar-width": "thin", + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-track": { + background: "transparent", + }, + "&::-webkit-scrollbar-thumb": { + borderRadius: "9999px", + }, + }; + + addUtilities({ + ".scrollbar-code-dark": { + ...scrollbarBase, + "scrollbar-color": "rgb(255 255 255 / 0.2) transparent", + "&::-webkit-scrollbar-thumb": { + borderRadius: "9999px", + backgroundColor: "rgb(255 255 255 / 0.2)", + }, + "&:hover::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.25)", + }, + "&:active::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.25)", + }, + }, + ".scrollbar-code-system": { + ...scrollbarBase, + "scrollbar-color": "rgb(0 0 0 / 0.15) transparent", + "&::-webkit-scrollbar-thumb": { + borderRadius: "9999px", + backgroundColor: "rgb(0 0 0 / 0.15)", + }, + "&:hover::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(0 0 0 / 0.2)", + }, + "&:active::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(0 0 0 / 0.2)", + }, + }, + ".dark .scrollbar-code-system": { + "scrollbar-color": "rgb(255 255 255 / 0.2) transparent", + "&::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.2)", + }, + "&:hover::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.25)", + }, + "&:active::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.25)", + }, + }, + ".scrollbar-common": { + ...scrollbarBase, + "scrollbar-color": "rgb(0 0 0 / 0.2) transparent", + "&::-webkit-scrollbar-thumb": { + borderRadius: "9999px", + backgroundColor: "rgb(0 0 0 / 0.2)", + }, + "&:hover::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(0 0 0 / 0.25)", + }, + "&:active::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(0 0 0 / 0.25)", + }, + }, + ".dark .scrollbar-common": { + "scrollbar-color": "rgb(255 255 255 / 0.2) transparent", + "&::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.2)", + }, + "&:hover::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.25)", + }, + "&:active::-webkit-scrollbar-thumb": { + backgroundColor: "rgb(255 255 255 / 0.25)", + }, + }, + }); +}); diff --git a/packages/components/src/styles.css b/packages/components/src/styles.css index 471da8e7..ac73fb9e 100644 --- a/packages/components/src/styles.css +++ b/packages/components/src/styles.css @@ -1,7 +1,9 @@ @import "tailwindcss"; +@import "./code.css"; @plugin "@tailwindcss/typography"; @plugin "./plugins/bg-grid.ts"; +@plugin "./plugins/scrollbar.ts"; @source "./**/*.{js,jsx,ts,tsx}"; diff --git a/packages/components/src/twoslash.css b/packages/components/src/twoslash.css new file mode 100644 index 00000000..1da89c14 --- /dev/null +++ b/packages/components/src/twoslash.css @@ -0,0 +1,320 @@ +/* modified from https://github.com/shikijs/shiki/blob/main/packages/twoslash/style-rich.css */ +/* ===== Basic ===== */ +:root { + --twoslash-border-color: #dbdfde; + --twoslash-underline-color: currentColor; + --twoslash-popup-bg: #f3f7f6; + --twoslash-popup-color: inherit; + --twoslash-popup-shadow: rgba(0, 0, 0, 0.08) 0px 1px 4px; + --twoslash-code-font: inherit; + --twoslash-code-font-size: 1em; + --twoslash-matched-color: inherit; + + --twoslash-highlighted-border: #c37d0d50; + --twoslash-highlighted-bg: #c37d0d20; + --twoslash-unmatched-color: #888; + --twoslash-cursor-color: #8888; + --twoslash-error-color: #d45656; + --twoslash-error-bg: #d4565620; + --twoslash-warn-color: #c37d0d; + --twoslash-warn-bg: #c37d0d20; + --twoslash-tag-color: #3772cf; + --twoslash-tag-bg: #3772cf20; + --twoslash-tag-warn-color: var(--twoslash-warn-color); + --twoslash-tag-warn-bg: var(--twoslash-warn-bg); + --twoslash-tag-annotate-color: #1ba673; + --twoslash-tag-annotate-bg: #1ba67320; + --twoslash-text-size: 0.8rem; + --twoslash-docs-tag-style: italic; +} + +:root.twoslash-dark, +html.dark div.twoslash-dark, +html.dark div.dark\:twoslash-dark { + --twoslash-border-color: #222526; + --twoslash-popup-bg: #151819; + --twoslash-highlighted-border: #ffa50080; + --twoslash-highlighted-bg: #ffa50030; + --twoslash-unmatched-color: #aaa; + --twoslash-cursor-color: #bbbb; + --twoslash-error-color: #ff6b6b; + --twoslash-error-bg: #ff6b6b30; + --twoslash-warn-color: #ffa500; + --twoslash-warn-bg: #ffa50030; + --twoslash-tag-color: #6bb6ff; + --twoslash-tag-bg: #6bb6ff30; + --twoslash-tag-warn-color: var(--twoslash-warn-color); + --twoslash-tag-warn-bg: var(--twoslash-warn-bg); + --twoslash-tag-annotate-color: #4ade80; + --twoslash-tag-annotate-bg: #4ade8030; +} + +/* Respect people's wishes to not have animations */ +@media (prefers-reduced-motion: reduce) { + .twoslash * { + transition: none !important; + } +} + +/* ===== Hover Info ===== */ +.twoslash:hover .twoslash-hover { + border-color: var(--twoslash-underline-color); +} + +.twoslash .twoslash-hover { + border-bottom: 1px dotted transparent; + transition: border-color 0.3s; + transition-timing-function: ease; + position: relative; +} + +.twoslash a span.twoslash-hover { + border-bottom: 1px solid var(--twoslash-underline-color); + position: relative; +} + +.twoslash a span.twoslash-hover:hover { + opacity: 0.75; +} + +.twoslash-popup-container .code-block { + width: 100%; + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.mint-twoslash-popover div[data-component-part="code-block-root"] { + width: 100%; +} + +/* ===== Popup Override ===== */ +[data-radix-popper-content-wrapper] { + z-index: 9999 !important; +} + +.mint-twoslash-popover { + background: var(--twoslash-popup-bg); + color: var(--twoslash-popup-color); + border: 1px solid var(--twoslash-border-color); + border-radius: 10px; + font-size: var(--twoslash-text-size) !important; + pointer-events: auto; + text-align: left; + box-shadow: var(--twoslash-popup-shadow); + display: inline-flex; + flex-direction: column; + padding: 6px; + max-width: 50vw; +} + +.mint-twoslash-popover-pre { + width: 100%; + display: flex; + font-size: var(--twoslash-text-size); + font-family: var(--twoslash-code-font); + font-weight: 400; +} + +.mint-twoslash-popover code { + padding: 0 !important; + background: transparent !important; +} + +code.twoslash-popup-code.shiki { + padding: 8px !important; +} + +.mint-twoslash-popover:hover { + user-select: auto; +} + +.twoslash .twoslash-popup-arrow { + display: none; +} + +.twoslash-popup-code, +.twoslash-popup-error, +.twoslash-popup-docs { + padding: 6px 8px !important; +} + +.mint-twoslash-popover .twoslash-popup-docs { + max-width: unset; +} + +.mint-twoslash-popover .twoslash-popup-error { + color: var(--twoslash-error-color); + background-color: var(--twoslash-error-bg); +} + +.mint-twoslash-popover .twoslash-popup-docs-tags { + display: flex; + flex-direction: column; +} + +.mint-twoslash-popover .twoslash-popup-docs-tag-name { + margin-right: 0.5em; + font-style: var(--twoslash-docs-tag-style); +} + +.mint-twoslash-popover .twoslash-popup-docs-tag-name { + font-family: var(--twoslash-code-font); +} + +/* ===== Query Line ===== */ +.mint-twoslash-popover .twoslash-query-line .twoslash-popup-container { + position: relative; + margin-bottom: 1.4em; + transform: translateY(0.6em); +} + +div.twoslash-meta-line.twoslash-query-line { + display: contents; +} + +/* ===== Error Line ===== */ +.twoslash-error-line { + position: relative; + background-color: var(--twoslash-error-bg); + border-left: 3px solid var(--twoslash-error-color); + color: var(--twoslash-error-color); + padding: 6px 12px; + margin: 0.2em 0; + min-width: 100%; + width: max-content; +} + +.twoslash-error-line.twoslash-error-level-warning { + background-color: var(--twoslash-warn-bg); + border-left: 3px solid var(--twoslash-warn-color); + color: var(--twoslash-warn-color); +} + +.mint-twoslash-popover .twoslash-error { + background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") + repeat-x bottom left; + padding-bottom: 2px; +} + +.mint-twoslash-popover .twoslash-error.twoslash-error-level-warning { + background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c37d0d'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") + repeat-x bottom left; + padding-bottom: 2px; +} + +/* ===== Completeions ===== */ +.mint-twoslash-popover .twoslash-completion-cursor { + position: relative; +} + +.mint-twoslash-popover .twoslash-completion-cursor .twoslash-completion-list { + user-select: none; + position: absolute; + top: 0; + left: 0; + transform: translate(0, 1.2em); + margin: 3px 0 0 -1px; + display: inline-block; + z-index: 8; + box-shadow: var(--twoslash-popup-shadow); + background: var(--twoslash-popup-bg); + border: 1px solid var(--twoslash-border-color); +} + +.twoslash-completion-list { + width: 240px; + font-size: var(--twoslash-text-size); + padding: 4px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.twoslash-completion-list:hover { + user-select: auto; +} + +.twoslash-completion-list::before { + background-color: var(--twoslash-cursor-color); + width: 2px; + position: absolute; + top: -1.6em; + height: 1.4em; + left: -1px; + content: " "; +} + +.twoslash-completion-list li { + overflow: hidden; + display: flex; + align-items: center; + gap: 0.25em; + line-height: 1em; +} + +.twoslash-completion-list li span.twoslash-completions-unmatched { + color: var(--twoslash-unmatched-color); +} + +.twoslash-completion-list .deprecated { + text-decoration: line-through; + opacity: 0.5; +} + +.twoslash-completion-list li span.twoslash-completions-matched { + color: var(--twoslash-matched-color); +} + +/* Highlights */ +.twoslash-highlighted { + background-color: var(--twoslash-highlighted-bg); + border: 1px solid var(--twoslash-highlighted-border); + padding: 1px 2px; + margin: -1px -3px; + border-radius: 4px; +} + +/* Icons */ +.twoslash-completion-list .twoslash-completions-icon { + color: var(--twoslash-unmatched-color); + width: 1em; + flex: none; +} + +/* Custom Tags */ +.mint-twoslash-popover .twoslash-tag-line { + position: relative; + background-color: var(--twoslash-tag-bg); + border-left: 3px solid var(--twoslash-tag-color); + color: var(--twoslash-tag-color); + padding: 6px 10px; + margin: 0.2em 0; + display: flex; + align-items: center; + gap: 0.3em; + min-width: 100%; + width: max-content; +} + +.mint-twoslash-popover .twoslash-tag-line .twoslash-tag-icon { + width: 1.1em; + color: inherit; +} + +.twoslash-tag-line.twoslash-tag-error-line { + background-color: var(--twoslash-error-bg); + border-left: 3px solid var(--twoslash-error-color); + color: var(--twoslash-error-color); +} + +.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-warn-line { + background-color: var(--twoslash-tag-warn-bg); + border-left: 3px solid var(--twoslash-tag-warn-color); + color: var(--twoslash-tag-warn-color); +} + +.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-annotate-line { + background-color: var(--twoslash-tag-annotate-bg); + border-left: 3px solid var(--twoslash-tag-annotate-color); + color: var(--twoslash-tag-annotate-color); +} diff --git a/packages/components/src/utils/get-node-text.ts b/packages/components/src/utils/get-node-text.ts new file mode 100644 index 00000000..aa9729f7 --- /dev/null +++ b/packages/components/src/utils/get-node-text.ts @@ -0,0 +1,26 @@ +// Gets the text from a component as if you selected it with a mouse and copied it. +const getNodeText = (node: unknown): string => { + if (typeof node === "string" || typeof node === "number") { + // Convert number into string + return node.toString(); + } + + if (Array.isArray(node)) { + return node.map(getNodeText).join(""); + } + + if ( + node && + typeof node === "object" && + "props" in node && + node.props && + typeof node.props === "object" && + "children" in node.props + ) { + return getNodeText(node.props.children); + } + + return ""; +}; + +export { getNodeText }; diff --git a/packages/components/src/utils/icon-utils.ts b/packages/components/src/utils/icon-utils.ts index b50dc496..abf707c3 100644 --- a/packages/components/src/utils/icon-utils.ts +++ b/packages/components/src/utils/icon-utils.ts @@ -1,6 +1,7 @@ import { MINTLIFY_ICONS_CDN_URL } from "@/constants"; import { FONT_AWESOME_BRANDS } from "@/constants/icons/font-awesome/v7.1.0/brands"; import type { IconLibrary, IconType } from "@/models"; +import { getIconKey } from "@/utils/shiki/snippet-presets"; const getIconUrl = ( icon: string, @@ -11,6 +12,11 @@ const getIconUrl = ( return `${MINTLIFY_ICONS_CDN_URL}/lucide/v0.545.0/${icon}.svg`; } + const languageIconUrl = getLanguageIconUrl(icon); + if (languageIconUrl) { + return languageIconUrl; + } + if (isBrandsIcon(icon)) { return `${MINTLIFY_ICONS_CDN_URL}/v7.1.0/brands/${icon}.svg`; } @@ -28,4 +34,12 @@ const isBrandsIcon = (icon?: string): boolean => { return FONT_AWESOME_BRANDS.includes(icon.toLowerCase()); }; -export { getIconUrl, isBrandsIcon }; +const getLanguageIconUrl = (language: string): string | null => { + const iconKey = getIconKey(language); + if (!iconKey) { + return null; + } + return `${MINTLIFY_ICONS_CDN_URL}/devicon/${iconKey}.svg`; +}; + +export { getIconUrl, isBrandsIcon, getLanguageIconUrl }; diff --git a/packages/components/src/utils/shiki/constants.ts b/packages/components/src/utils/shiki/constants.ts new file mode 100644 index 00000000..f4880104 --- /dev/null +++ b/packages/components/src/utils/shiki/constants.ts @@ -0,0 +1,173 @@ +import type { BuiltinTheme } from "shiki/types"; +import { SHIKI_THEMES } from "@/mdx"; + +const DEFAULT_DARK_THEME = "dark-plus"; +const DEFAULT_LIGHT_THEME = "github-light-default"; +type ShikiThemeType = (typeof SHIKI_THEMES)[number]; + +const LINE_HIGHLIGHT_CLASS_NAME = "line-highlight"; +const LINE_FOCUS_CLASS_NAME = "line-focus"; +const LINE_DIFF_ADD_CLASS_NAME = "line-diff line-add"; +const LINE_DIFF_REMOVE_CLASS_NAME = "line-diff line-remove"; + +const SHIKI_THEMES_EXCLUDING_CSS_VARIABLES_AND_DEFAULTS = SHIKI_THEMES.filter( + (theme) => + theme !== "css-variables" && + theme !== DEFAULT_DARK_THEME && + theme !== DEFAULT_LIGHT_THEME +); + +const DEFAULT_DARK_BG = "#0B0C0E"; +const DEFAULT_LIGHT_BG = "#FFFFFF"; + +const THEMES: [BuiltinTheme, BuiltinTheme, ...BuiltinTheme[]] = [ + DEFAULT_DARK_THEME, + DEFAULT_LIGHT_THEME, + ...SHIKI_THEMES_EXCLUDING_CSS_VARIABLES_AND_DEFAULTS, +] as const; + +const LANGS = [ + "bash" as const, + "c" as const, + "c++" as const, + "dart" as const, + "go" as const, + "java" as const, + "javascript" as const, + "json" as const, + "kotlin" as const, + "php" as const, + "python" as const, + "ruby" as const, + "rust" as const, + "swift" as const, + "csharp" as const, + "typescript" as const, + "tsx" as const, + "yaml" as const, +]; + +type ShikiTheme = BuiltinTheme; +type ShikiLanguage = (typeof LANGS)[number]; + +const SHIKI_COLOR_REPLACEMENTS: Record< + string, + string | Record +> = { + [THEMES[0]]: { + "#1e1e1e": DEFAULT_DARK_BG, + }, +}; + +const SHIKI_THEME_MAP: Record<"dark" | "light", ShikiTheme> = { + dark: THEMES[0], + light: THEMES[1], +} as const; + +const SHIKI_LANG_MAP: Record = { + curl: "bash", + bash: "bash", + sh: "bash", + shell: "bash", + zsh: "bash", + shellscript: "bash", + + c: "c", + csharp: "csharp", + + "c++": "c++", + cpp: "c++", + cc: "c++", + + go: "go", + golang: "go", + + java: "java", + + javascript: "javascript", + js: "javascript", + node: "javascript", + nodejs: "javascript", + + json: "json", + jsonc: "json", + json5: "json", + + php: "php", + + python: "python", + py: "python", + + typescript: "typescript", + ts: "typescript", + + tsx: "tsx", + react: "tsx", + reactts: "tsx", + "react-ts": "tsx", + jsx: "tsx", + + ruby: "ruby", + rb: "ruby", + + rust: "rust", + rs: "rust", + rustc: "rust", + + swift: "swift", + kotlin: "kotlin", + kt: "kotlin", + dart: "dart", + flutter: "dart", + + yaml: "yaml", + yml: "yaml", + toml: "yaml", +} as const; + +const SHIKI_DISPLAY_LANG_MAP: Record = { + bash: "cURL", + c: "C", + "c++": "C++", + "c#": "C#", + ".NET": ".NET", + dart: "Dart", + go: "Go", + java: "Java", + javascript: "Javascript", + json: "JSON", + kotlin: "Kotlin", + node: "Node.js", + php: "PHP", + python: "Python", + ruby: "Ruby", + rust: "Rust", + swift: "Swift", + typescript: "Typescript", + tsx: "TSX", + yaml: "YAML", +} as const; + +const SHIKI_CLASSNAME = "shiki shiki-themes"; + +export { + SHIKI_CLASSNAME, + SHIKI_COLOR_REPLACEMENTS, + SHIKI_DISPLAY_LANG_MAP, + SHIKI_LANG_MAP, + SHIKI_THEME_MAP, + type ShikiTheme, + type ShikiLanguage, + THEMES, + LANGS, + DEFAULT_DARK_BG, + DEFAULT_LIGHT_BG, + LINE_HIGHLIGHT_CLASS_NAME, + LINE_FOCUS_CLASS_NAME, + LINE_DIFF_ADD_CLASS_NAME, + LINE_DIFF_REMOVE_CLASS_NAME, + SHIKI_THEMES_EXCLUDING_CSS_VARIABLES_AND_DEFAULTS, + DEFAULT_DARK_THEME, + DEFAULT_LIGHT_THEME, + type ShikiThemeType, +}; diff --git a/packages/components/src/utils/shiki/get-shiki-background-colors.ts b/packages/components/src/utils/shiki/get-shiki-background-colors.ts new file mode 100644 index 00000000..7ec748a8 --- /dev/null +++ b/packages/components/src/utils/shiki/get-shiki-background-colors.ts @@ -0,0 +1,83 @@ +import { DEFAULT_DARK_BG, DEFAULT_LIGHT_BG } from "@/utils/shiki/constants"; + +const getShikiBackgroundColors = ( + codeBlockTheme: "system" | "dark", + html: string | undefined, + children: unknown +): { light: string | undefined; dark: string | undefined } | undefined => { + const defaultBgColors = + codeBlockTheme === "dark" + ? { light: DEFAULT_DARK_BG, dark: DEFAULT_DARK_BG } + : { light: DEFAULT_LIGHT_BG, dark: DEFAULT_DARK_BG }; + if (html) { + return getBackgroundColorsFromHtml(html, defaultBgColors); + } + if (children) { + return getBackgroundColorsFromChildren(children, defaultBgColors); + } + return defaultBgColors; +}; + +const isTransparentOrUndefined = (color: string | undefined) => + color === "transparent" || color === undefined; + +const getBackgroundColorsFromHtml = ( + html: string, + defaultBgColors: { light: string; dark: string } +): { light: string | undefined; dark: string | undefined } => { + const styles = html.split('style="'); + if (styles.length < 2) { + return defaultBgColors; + } + const backgroundColor = styles[1] + ?.split("background-color:")[1] + ?.split(";")[0]; + const shikiDarkBgValue = styles[1] + ?.split("--shiki-dark-bg:")[1] + ?.split(";")[0]; + return { + light: isTransparentOrUndefined(backgroundColor) + ? defaultBgColors.light + : backgroundColor, + dark: isTransparentOrUndefined(shikiDarkBgValue) + ? defaultBgColors.dark + : shikiDarkBgValue, + }; +}; + +const getBackgroundColorsFromChildren = ( + children: unknown, + defaultBgColors: { light: string; dark: string } +): { light: string | undefined; dark: string | undefined } => { + if ( + children && + typeof children === "object" && + "props" in children && + children.props && + typeof children.props === "object" && + "style" in children.props && + typeof children.props.style === "object" && + children.props.style && + typeof children.props.style === "object" && + "backgroundColor" in children.props.style && + typeof children.props.style.backgroundColor === "string" && + "--shiki-dark-bg" in children.props.style && + typeof children.props.style["--shiki-dark-bg"] === "string" + ) { + return { + light: isTransparentOrUndefined(children.props.style.backgroundColor) + ? defaultBgColors.light + : children.props.style.backgroundColor, + dark: isTransparentOrUndefined(children.props.style["--shiki-dark-bg"]) + ? defaultBgColors.dark + : children.props.style["--shiki-dark-bg"], + }; + } + return defaultBgColors; +}; + +export { + getShikiBackgroundColors, + getBackgroundColorsFromHtml, + getBackgroundColorsFromChildren, +}; diff --git a/packages/components/src/utils/shiki/index.ts b/packages/components/src/utils/shiki/index.ts new file mode 100644 index 00000000..9be790cd --- /dev/null +++ b/packages/components/src/utils/shiki/index.ts @@ -0,0 +1,317 @@ +import { + transformerMetaHighlight, + transformerNotationDiff, + transformerNotationFocus, + transformerNotationHighlight, +} from "@shikijs/transformers"; +import type { ShikiTransformer } from "@shikijs/types"; +import type { Element, Root } from "hast"; +import { createHighlighter, hastToHtml } from "shiki"; +import { createJavaScriptRegexEngine } from "shiki/engine/javascript"; +import type { + BundledLanguage, + BundledTheme, + CodeToHastOptions, + HighlighterGeneric, +} from "shiki/types"; +import { codeStylingToThemeOrThemes } from "@/common/get-code-styling"; +import { MAX_PREVIEW_BYTES } from "@/constants"; +import { getShikiLanguageFromPreset } from "@/utils/shiki/snippet-presets"; +import type { CodeStyling } from "@/validation"; + +import { + LANGS, + LINE_DIFF_ADD_CLASS_NAME, + LINE_DIFF_REMOVE_CLASS_NAME, + LINE_FOCUS_CLASS_NAME, + LINE_HIGHLIGHT_CLASS_NAME, + SHIKI_COLOR_REPLACEMENTS, + SHIKI_LANG_MAP, + type ShikiLanguage, +} from "./constants"; +import { getShikiWorker } from "./worker-client"; + +const matchAlgorithm = { + matchAlgorithm: "v3", +} as const; + +const SHIKI_TRANSFORMERS: ShikiTransformer[] = [ + transformerMetaHighlight({ + className: LINE_HIGHLIGHT_CLASS_NAME, + }), + transformerNotationHighlight({ + ...matchAlgorithm, + classActiveLine: LINE_HIGHLIGHT_CLASS_NAME, + }), + transformerNotationFocus({ + ...matchAlgorithm, + classActiveLine: LINE_FOCUS_CLASS_NAME, + }), + transformerNotationDiff({ + ...matchAlgorithm, + classLineAdd: LINE_DIFF_ADD_CLASS_NAME, + classLineRemove: LINE_DIFF_REMOVE_CLASS_NAME, + }), +]; + +const jsEngine = createJavaScriptRegexEngine({ + forgiving: true, + cache: new Map(), +}); + +let highlighter: HighlighterGeneric | undefined; +let highlighterError: Error | undefined; +let highlighterResolved = false; +const highlighterPromise = createHighlighter({ + themes: [], + langs: LANGS, + engine: jsEngine, +}) + .then((hl) => { + highlighter = hl; + }) + .catch((err) => { + console.error(err); + highlighterError = + err ?? new Error("Unknown error occurred initializing highlighter"); + }) + .finally(() => { + highlighterResolved = true; + }); + +function getShikiLanguage(lang: string | undefined): ShikiLanguage { + const text = "text" as ShikiLanguage; + if (lang === undefined) { + return text; + } + + // Handle HTTP status codes as JSON + const possibleStatusCode = Number(lang); + if ( + !Number.isNaN(possibleStatusCode) && + possibleStatusCode > 99 && + possibleStatusCode < 600 + ) { + return "json"; + } + + const lower = lang.toLowerCase(); + + // Check snippet preset system first (source of truth for API playground languages) + // e.g., "dotnet" → "csharp", "node" → "javascript" + const presetLang = getShikiLanguageFromPreset(lower); + + // Then check shiki lang map (handles general aliases like "py" → "python") + return SHIKI_LANG_MAP[presetLang] ?? SHIKI_LANG_MAP[lower] ?? text; +} + +function getLanguageFromClassName( + className: string | undefined, + fallback?: string +): string { + // biome-ignore lint/performance/useTopLevelRegex: TODO + const match = /language-(\w+)/.exec(className ?? ""); + return match ? (match[1] ?? "text") : (fallback ?? "text"); +} + +function getThemesForCodeStyling(codeblocks?: CodeStyling): string[] { + if (typeof codeblocks === "string" || !codeblocks) { + return codeblocks === "system" || !codeblocks + ? ["dark-plus", "github-light-default"] + : ["dark-plus"]; + } + const { theme } = codeblocks; + if (typeof theme === "string") { + return [theme]; + } + if (typeof theme === "object") { + return [theme.dark, theme.light]; + } + return ["dark-plus", "github-light-default"]; +} + +function areThemesLoaded(codeblocks?: CodeStyling): boolean { + if (!highlighter) { + return false; + } + const loaded = highlighter.getLoadedThemes(); + const needed = getThemesForCodeStyling(codeblocks); + return needed.every((t) => t === "css-variables" || loaded.includes(t)); +} + +async function loadShikiThemes(codeblocks?: CodeStyling) { + await highlighterPromise; + if (!highlighter) { + return; + } + + const loaded = highlighter.getLoadedThemes(); + const toLoad = getThemesForCodeStyling(codeblocks).filter( + (t) => t !== "css-variables" && !loaded.includes(t) + ) as [BundledTheme, ...BundledTheme[]]; + + if (toLoad.length > 0) { + await highlighter.loadTheme(...toLoad); + } +} + +type ShikiHighlightedHtmlArgs = { + codeString: string; + codeBlockTheme: CodeStyling; + opts?: Partial & { + highlightedLines?: number[]; + focusedLines?: number[]; + noAsync?: boolean; + }; +} & ( + | { + className: string | undefined; + fileName?: string; + } + | { + language: string; + } +); + +function getShikiHighlightedHtml( + props: ShikiHighlightedHtmlArgs & { + opts: ShikiHighlightedHtmlArgs["opts"] & { noAsync: true }; + } +): string | undefined; + +function getShikiHighlightedHtml( + props: ShikiHighlightedHtmlArgs +): string | undefined | Promise; + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO +function getShikiHighlightedHtml( + props: ShikiHighlightedHtmlArgs +): string | undefined | Promise { + if (!props.codeString || highlighterError !== undefined) { + return undefined; + } + + if (highlighter === undefined) { + if (props.opts?.noAsync || highlighterResolved) { + return undefined; + } + return highlighterPromise + .then(() => getShikiHighlightedHtml(props)) + .catch(() => undefined); + } + + if (!areThemesLoaded(props.codeBlockTheme)) { + if (props.opts?.noAsync) { + return undefined; + } + return loadShikiThemes(props.codeBlockTheme) + .then(() => getShikiHighlightedHtml(props)) + .catch(() => undefined); + } + + let lang: string; + if ("language" in props) { + if (props.language === "text") { + return undefined; + } + lang = props.language; + } else { + if (props.className === "lang-text") { + return undefined; + } + lang = getLanguageFromClassName(props.className, props.fileName); + } + + if (props.codeString.length > MAX_PREVIEW_BYTES * 5) { + return undefined; + } + + if (props.codeString.length > MAX_PREVIEW_BYTES) { + if (props.opts?.noAsync === true) { + return undefined; + } + + try { + const worker = getShikiWorker(); + return worker === undefined ? undefined : worker.highlight(props); + } catch { + return undefined; + } + } + + let html: string | Root | undefined; + if (lang) { + try { + const serializer = + !!props.opts?.highlightedLines?.length || + !!props.opts?.focusedLines?.length + ? "codeToHast" + : "codeToHtml"; + const code = props.codeString.trim(); + const themeOrThemes = codeStylingToThemeOrThemes(props.codeBlockTheme); + + html = highlighter[serializer](code, { + lang: getShikiLanguage(lang), + ...themeOrThemes, + colorReplacements: { ...SHIKI_COLOR_REPLACEMENTS }, + transformers: SHIKI_TRANSFORMERS, + tabindex: false, + ...props.opts, + }); + } catch (error) { + console.error("error getting shiki highlighted html", error); + } + } + + if (typeof html !== "object") { + return html; + } + + const firstChild = html.children[0]; + if (!firstChild) { + return undefined; + } + if (firstChild.type === "element" && firstChild.tagName === "pre") { + const codeElement = firstChild.children[0] as Element | undefined; + if (!codeElement?.children) { + return hastToHtml(html); + } + const spanElements = codeElement.children.filter( + (child) => child.type === "element" && child.tagName === "span" + ) as Element[]; + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO + spanElements.forEach((child, index) => { + // we used 1-based index in the meta string, so we need to increment it by 1 + const actualIndex = index + 1; + if (typeof child.properties.class === "string") { + if (props.opts?.highlightedLines?.includes(actualIndex)) { + child.properties.class += ` ${LINE_HIGHLIGHT_CLASS_NAME}`; + } + + if (props.opts?.focusedLines?.includes(actualIndex)) { + child.properties.class += ` ${LINE_FOCUS_CLASS_NAME}`; + } + } else if (Array.isArray(child.properties.class)) { + if (props.opts?.highlightedLines?.includes(actualIndex)) { + child.properties.class.push(LINE_HIGHLIGHT_CLASS_NAME); + } + + if (props.opts?.focusedLines?.includes(actualIndex)) { + child.properties.class.push(LINE_FOCUS_CLASS_NAME); + } + } + }); + } + + return hastToHtml(html); +} + +export { + SHIKI_TRANSFORMERS, + highlighterPromise, + getShikiLanguage, + getLanguageFromClassName, + loadShikiThemes, + type ShikiHighlightedHtmlArgs, + getShikiHighlightedHtml, +}; diff --git a/packages/components/src/utils/shiki/lib.ts b/packages/components/src/utils/shiki/lib.ts new file mode 100644 index 00000000..99c9bab0 --- /dev/null +++ b/packages/components/src/utils/shiki/lib.ts @@ -0,0 +1,79 @@ +import { type ReactNode, useMemo } from "react"; + +import { getNodeText } from "@/utils/get-node-text"; +import { SHIKI_CLASSNAME } from "@/utils/shiki/constants"; + +function findShikiClassName(children: unknown): boolean { + if (!children || typeof children !== "object") { + return false; + } + + if (Array.isArray(children)) { + for (const child of children) { + if (findShikiClassName(child)) { + return true; + } + } + } + + if ( + "props" in children && + children.props && + typeof children.props === "object" + ) { + if ( + "className" in children.props && + typeof children.props.className === "string" && + children.props.className.includes(SHIKI_CLASSNAME) + ) { + return true; + } + + if ("children" in children.props) { + return findShikiClassName(children.props.children); + } + } + + return false; +} + +function getCodeString( + children: ReactNode, + className?: string, + forceExtract = false +): string { + const isShiki = + className?.includes(SHIKI_CLASSNAME) || findShikiClassName(children); + if (isShiki && !forceExtract) { + return ""; + } + + const codeString = getNodeText(children); + + return codeString; +} + +function calculateCodeLinesFromHtml(html: string | undefined): number { + if (!html || typeof html !== "string") { + return 0; + } + + const lineMatches = html.match(/(); +for (const preset of SNIPPET_PRESETS) { + presetLookup.set(preset.key.toLowerCase(), preset); + for (const alias of preset.aliases ?? []) { + presetLookup.set(alias.toLowerCase(), preset); + } +} + +const getPreset = (lang: string): SnippetPreset | undefined => + presetLookup.get(lang.toLowerCase()); + +const getDisplayName = (lang: string): string => + getPreset(lang)?.displayName ?? lang; + +const getIconKey = (lang: string): string | undefined => { + const preset = getPreset(lang); + return preset ? (preset.iconKey ?? preset.key) : undefined; +}; + +const getShikiLanguageFromPreset = (lang: string): string => + getPreset(lang)?.shikiLanguage ?? lang; + +export { + type SnippetPreset, + SNIPPET_PRESETS, + getPreset, + getDisplayName, + getIconKey, + getShikiLanguageFromPreset, +}; diff --git a/packages/components/src/utils/shiki/worker-client.ts b/packages/components/src/utils/shiki/worker-client.ts new file mode 100644 index 00000000..969905c2 --- /dev/null +++ b/packages/components/src/utils/shiki/worker-client.ts @@ -0,0 +1,28 @@ +import { type Remote, wrap } from "comlink"; + +import type { ShikiHighlightedHtmlArgs } from "../shiki"; + +type HighlightFn = ( + props: ShikiHighlightedHtmlArgs +) => string | undefined | Promise; +type ShikiWorkerInstance = + | Remote<{ highlight: HighlightFn; ready: () => Promise }> + | undefined; + +let instance: ShikiWorkerInstance; + +function getShikiWorker(): ShikiWorkerInstance { + if (typeof Worker === "undefined") { + return undefined; + } + if (instance) { + return instance; + } + const worker = new Worker(new URL("./worker.js", import.meta.url), { + type: "module", + }); + instance = wrap(worker); + return instance; +} + +export { getShikiWorker }; diff --git a/packages/components/src/utils/shiki/worker.js b/packages/components/src/utils/shiki/worker.js new file mode 100644 index 00000000..ce23c143 --- /dev/null +++ b/packages/components/src/utils/shiki/worker.js @@ -0,0 +1,170 @@ +import { expose } from "comlink"; + +let replacements, langMap; +let transformers; +let highlighter; +let hastToHtmlFn; +let codeStylingToThemeOrThemesFn; +let getShikiLanguageFromPresetFn; +let lineHighlightClassName, lineFocusClassName; + +const ready = (async () => { + const [ + { createHighlighter, hastToHtml }, + { createJavaScriptRegexEngine }, + { + transformerNotationHighlight, + transformerNotationFocus, + transformerMetaHighlight, + transformerNotationDiff, + }, + { + THEMES, + LANGS, + SHIKI_COLOR_REPLACEMENTS, + SHIKI_LANG_MAP, + LINE_HIGHLIGHT_CLASS_NAME, + LINE_FOCUS_CLASS_NAME, + LINE_DIFF_ADD_CLASS_NAME, + LINE_DIFF_REMOVE_CLASS_NAME, + }, + { codeStylingToThemeOrThemes }, + { getShikiLanguageFromPreset }, + ] = await Promise.all([ + import("shiki"), + import("shiki/engine/javascript"), + import("@shikijs/transformers"), + import("./constants.ts"), + import("@/common/get-code-styling"), + import("@/utils/shiki/snippet-presets"), + ]); + + replacements = SHIKI_COLOR_REPLACEMENTS; + langMap = SHIKI_LANG_MAP; + hastToHtmlFn = hastToHtml; + lineHighlightClassName = LINE_HIGHLIGHT_CLASS_NAME; + lineFocusClassName = LINE_FOCUS_CLASS_NAME; + codeStylingToThemeOrThemesFn = codeStylingToThemeOrThemes; + getShikiLanguageFromPresetFn = getShikiLanguageFromPreset; + const engine = createJavaScriptRegexEngine({ + forgiving: true, + cache: new Map(), + }); + highlighter = await createHighlighter({ + themes: THEMES, + langs: LANGS, + engine, + }); + + const matchAlgorithm = { + matchAlgorithm: "v3", + }; + + transformers = [ + transformerMetaHighlight({ + className: LINE_HIGHLIGHT_CLASS_NAME, + }), + transformerNotationHighlight({ + ...matchAlgorithm, + classActiveLine: LINE_HIGHLIGHT_CLASS_NAME, + }), + transformerNotationFocus({ + ...matchAlgorithm, + classActiveLine: LINE_FOCUS_CLASS_NAME, + }), + transformerNotationDiff({ + ...matchAlgorithm, + classLineAdd: LINE_DIFF_ADD_CLASS_NAME, + classLineRemove: LINE_DIFF_REMOVE_CLASS_NAME, + }), + ]; +})(); + +function getShikiLanguage(lang) { + const text = "text"; + if (!lang) { + return text; + } + + const n = Number(lang); + const isStatus = !Number.isNaN(n) && Number.isFinite(n) && n > 99 && n < 600; + if (isStatus) { + return "json"; + } + + const lower = lang.toLowerCase(); + const presetLang = getShikiLanguageFromPresetFn(lower); + + return langMap[presetLang] ?? langMap[lower] ?? text; +} + +function getLanguageFromClassName(className, fallback) { + // biome-ignore lint/performance/useTopLevelRegex: TODO + const m = /language-(\w+)/.exec(className ?? ""); + return m ? (m[1] ?? "text") : (fallback ?? "text"); +} + +function highlightSync(opts) { + const lang = + "language" in opts + ? opts.language + : getLanguageFromClassName(opts.className, opts.fileName); + let html; + if (lang) { + const serializer = + !!opts.opts?.highlightedLines?.length || !!opts.opts?.focusedLines?.length + ? "codeToHast" + : "codeToHtml"; + const code = opts.codeString.trim(); + const themeOrThemes = codeStylingToThemeOrThemesFn(opts.codeBlockTheme); + html = highlighter[serializer](code, { + lang: getShikiLanguage(lang), + ...themeOrThemes, + colorReplacements: { ...replacements }, + transformers, + tabindex: false, + ...opts.opts, + }); + } + if (typeof html !== "object") { + return html; + } + const pre = html.children[0]; + if (pre?.type === "element" && pre.tagName === "pre") { + const codeElement = pre.children[0]; + if (!codeElement?.children) { + return hastToHtmlFn(html); + } + const spanElements = codeElement.children.filter( + (child) => child.type === "element" && child.tagName === "span" + ); + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO + spanElements.forEach((c, i) => { + if (typeof c.properties.class === "string") { + if (opts.opts?.highlightedLines?.includes(i + 1)) { + c.properties.class += ` ${lineHighlightClassName}`; + } + + if (opts.opts?.focusedLines?.includes(i + 1)) { + c.properties.class += ` ${lineFocusClassName}`; + } + } else if (Array.isArray(c.properties.class)) { + if (opts.opts?.highlightedLines?.includes(i + 1)) { + c.properties.class.push(lineHighlightClassName); + } + + if (opts.opts?.focusedLines?.includes(i + 1)) { + c.properties.class.push(lineFocusClassName); + } + } + }); + } + return hastToHtmlFn(html); +} + +async function highlight(props) { + await ready; + return highlightSync(props); +} + +expose({ highlight, ready }); diff --git a/packages/components/src/validation/index.ts b/packages/components/src/validation/index.ts new file mode 100644 index 00000000..ccb3c6a6 --- /dev/null +++ b/packages/components/src/validation/index.ts @@ -0,0 +1,202 @@ +type CodeStyling = + | "dark" + | "system" + | { + languages?: + | { + custom?: string[] | undefined; + } + | undefined; + theme?: + | "andromeeda" + | "aurora-x" + | "ayu-dark" + | "catppuccin-frappe" + | "catppuccin-latte" + | "catppuccin-macchiato" + | "catppuccin-mocha" + | "dark-plus" + | "dracula" + | "dracula-soft" + | "everforest-dark" + | "everforest-light" + | "github-dark" + | "github-dark-default" + | "github-dark-dimmed" + | "github-dark-high-contrast" + | "github-light" + | "github-light-default" + | "github-light-high-contrast" + | "gruvbox-dark-hard" + | "gruvbox-dark-medium" + | "gruvbox-dark-soft" + | "gruvbox-light-hard" + | "gruvbox-light-medium" + | "gruvbox-light-soft" + | "houston" + | "kanagawa-dragon" + | "kanagawa-lotus" + | "kanagawa-wave" + | "laserwave" + | "light-plus" + | "material-theme" + | "material-theme-darker" + | "material-theme-lighter" + | "material-theme-ocean" + | "material-theme-palenight" + | "min-dark" + | "min-light" + | "monokai" + | "night-owl" + | "nord" + | "one-dark-pro" + | "one-light" + | "plastic" + | "poimandres" + | "red" + | "rose-pine" + | "rose-pine-dawn" + | "rose-pine-moon" + | "slack-dark" + | "slack-ochin" + | "snazzy-light" + | "solarized-dark" + | "solarized-light" + | "synthwave-84" + | "tokyo-night" + | "vesper" + | "vitesse-black" + | "vitesse-dark" + | "vitesse-light" + | "css-variables" + | { + light: + | "andromeeda" + | "aurora-x" + | "ayu-dark" + | "catppuccin-frappe" + | "catppuccin-latte" + | "catppuccin-macchiato" + | "catppuccin-mocha" + | "dark-plus" + | "dracula" + | "dracula-soft" + | "everforest-dark" + | "everforest-light" + | "github-dark" + | "github-dark-default" + | "github-dark-dimmed" + | "github-dark-high-contrast" + | "github-light" + | "github-light-default" + | "github-light-high-contrast" + | "gruvbox-dark-hard" + | "gruvbox-dark-medium" + | "gruvbox-dark-soft" + | "gruvbox-light-hard" + | "gruvbox-light-medium" + | "gruvbox-light-soft" + | "houston" + | "kanagawa-dragon" + | "kanagawa-lotus" + | "kanagawa-wave" + | "laserwave" + | "light-plus" + | "material-theme" + | "material-theme-darker" + | "material-theme-lighter" + | "material-theme-ocean" + | "material-theme-palenight" + | "min-dark" + | "min-light" + | "monokai" + | "night-owl" + | "nord" + | "one-dark-pro" + | "one-light" + | "plastic" + | "poimandres" + | "red" + | "rose-pine" + | "rose-pine-dawn" + | "rose-pine-moon" + | "slack-dark" + | "slack-ochin" + | "snazzy-light" + | "solarized-dark" + | "solarized-light" + | "synthwave-84" + | "tokyo-night" + | "vesper" + | "vitesse-black" + | "vitesse-dark" + | "vitesse-light" + | "css-variables"; + dark: + | "andromeeda" + | "aurora-x" + | "ayu-dark" + | "catppuccin-frappe" + | "catppuccin-latte" + | "catppuccin-macchiato" + | "catppuccin-mocha" + | "dark-plus" + | "dracula" + | "dracula-soft" + | "everforest-dark" + | "everforest-light" + | "github-dark" + | "github-dark-default" + | "github-dark-dimmed" + | "github-dark-high-contrast" + | "github-light" + | "github-light-default" + | "github-light-high-contrast" + | "gruvbox-dark-hard" + | "gruvbox-dark-medium" + | "gruvbox-dark-soft" + | "gruvbox-light-hard" + | "gruvbox-light-medium" + | "gruvbox-light-soft" + | "houston" + | "kanagawa-dragon" + | "kanagawa-lotus" + | "kanagawa-wave" + | "laserwave" + | "light-plus" + | "material-theme" + | "material-theme-darker" + | "material-theme-lighter" + | "material-theme-ocean" + | "material-theme-palenight" + | "min-dark" + | "min-light" + | "monokai" + | "night-owl" + | "nord" + | "one-dark-pro" + | "one-light" + | "plastic" + | "poimandres" + | "red" + | "rose-pine" + | "rose-pine-dawn" + | "rose-pine-moon" + | "slack-dark" + | "slack-ochin" + | "snazzy-light" + | "solarized-dark" + | "solarized-light" + | "synthwave-84" + | "tokyo-night" + | "vesper" + | "vitesse-black" + | "vitesse-dark" + | "vitesse-light" + | "css-variables"; + } + | undefined; + } + | undefined; + +export type { CodeStyling }; diff --git a/packages/components/vite.config.ts b/packages/components/vite.config.ts index 1e8dd6e7..ed06afea 100644 --- a/packages/components/vite.config.ts +++ b/packages/components/vite.config.ts @@ -24,6 +24,9 @@ export default defineConfig({ "@": resolve(__dirname, "./src"), }, }, + worker: { + format: "es", + }, build: { lib: { entry: resolve(__dirname, "src/index.ts"), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9593007..fa4fcf4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.4 version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@shikijs/transformers': + specifier: ^3.21.0 + version: 3.21.0 '@sindresorhus/slugify': specifier: ^2.1.1 version: 2.2.1 @@ -29,6 +32,12 @@ importers: color: specifier: ^4.2.3 version: 4.2.3 + comlink: + specifier: ^4.4.2 + version: 4.4.2 + hast: + specifier: ^1.0.0 + version: 1.0.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -38,10 +47,16 @@ importers: mermaid: specifier: ^11.12.2 version: 11.12.2 + shiki: + specifier: ^3.17.0 + version: 3.21.0 tailwind-merge: specifier: ^2.6.0 version: 2.6.0 devDependencies: + '@shikijs/types': + specifier: ^3.18.0 + version: 3.21.0 '@storybook/addon-docs': specifier: ^10.1.10 version: 10.1.11(@types/react@18.3.27)(esbuild@0.27.2)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) @@ -60,6 +75,9 @@ importers: '@types/color': specifier: ^4.2.0 version: 4.2.0 + '@types/hast': + specifier: ^3.0.4 + version: 3.0.4 '@types/lodash': specifier: 4.17.23 version: 4.17.23 @@ -1117,6 +1135,30 @@ packages: '@rushstack/ts-command-line@5.1.3': resolution: {integrity: sha512-Kdv0k/BnnxIYFlMVC1IxrIS0oGQd4T4b7vKfx52Y2+wk2WZSDFIvedr7JrhenzSlm3ou5KwtoTGTGd5nbODRug==} + '@shikijs/core@3.21.0': + resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==} + + '@shikijs/engine-javascript@3.21.0': + resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==} + + '@shikijs/engine-oniguruma@3.21.0': + resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} + + '@shikijs/langs@3.21.0': + resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} + + '@shikijs/themes@3.21.0': + resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} + + '@shikijs/transformers@3.21.0': + resolution: {integrity: sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==} + + '@shikijs/types@3.21.0': + resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/slugify@2.2.1': resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==} engines: {node: '>=12'} @@ -1441,12 +1483,18 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/lodash@4.17.23': resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -1470,6 +1518,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.47.0': resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1529,6 +1580,9 @@ packages: resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1699,6 +1753,9 @@ packages: caniuse-lite@1.0.30001756: resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1707,6 +1764,12 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -1740,6 +1803,11 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + comlink@4.4.2: + resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} @@ -1993,6 +2061,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -2221,10 +2292,23 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hast@1.0.0: + resolution: {integrity: sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==} + deprecated: Renamed to rehype + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2498,6 +2582,13 @@ packages: engines: {node: '>= 20'} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2505,6 +2596,21 @@ packages: mermaid@11.12.2: resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2570,6 +2676,12 @@ packages: engines: {node: '>=18'} hasBin: true + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -2663,6 +2775,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2706,6 +2821,23 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2775,6 +2907,25 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@3.21.0: + resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2793,6 +2944,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -2817,6 +2971,21 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2889,6 +3058,8 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} trpc-cli@0.12.2: resolution: {integrity: sha512-kGNCiyOimGlfcZFImbWzFF2Nn3TMnenwUdyuckiN5SEaceJbIac7+Iau3WsVHjQpoNgugFruZMDOKf8GNQNtJw==} engines: {node: '>=18'} @@ -2965,6 +3136,21 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2994,6 +3180,12 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -3112,6 +3304,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} zod@4.3.5: resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} @@ -3940,6 +4134,44 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@shikijs/core@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/themes@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/transformers@3.21.0': + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/types': 3.21.0 + + '@shikijs/types@3.21.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/slugify@2.2.1': dependencies: '@sindresorhus/transliterate': 1.6.0 @@ -4311,10 +4543,18 @@ snapshots: '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/lodash@4.17.23': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mdx@2.0.13': {} '@types/node@22.19.1': @@ -4337,6 +4577,8 @@ snapshots: '@types/trusted-types@2.0.7': optional: true + '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4430,6 +4672,8 @@ snapshots: '@typescript-eslint/types': 8.47.0 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))': dependencies: '@babel/core': 7.28.5 @@ -4617,6 +4861,8 @@ snapshots: caniuse-lite@1.0.30001756: {} + ccount@2.0.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -4630,6 +4876,10 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + check-error@2.1.3: {} chevrotain-allstar@0.3.1(chevrotain@11.0.3): @@ -4666,6 +4916,9 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + comlink@4.4.2: {} + + comma-separated-tokens@2.0.3: {} commander@14.0.2: {} commander@7.2.0: {} @@ -4917,6 +5170,10 @@ snapshots: detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.2: {} doctrine@3.0.0: @@ -5194,8 +5451,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast@1.0.0: {} + he@1.2.0: {} + html-void-elements@3.0.0: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -5405,6 +5684,20 @@ snapshots: marked@16.4.2: {} + math-intrinsics@1.1.0: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + merge2@1.4.1: {} mermaid@11.12.2: @@ -5430,6 +5723,23 @@ snapshots: ts-dedent: 2.2.0 uuid: 11.1.0 + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5487,6 +5797,14 @@ snapshots: pathe: 2.0.3 tinyexec: 1.0.2 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + open@10.2.0: dependencies: default-browser: 5.4.0 @@ -5582,6 +5900,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + property-information@7.1.0: {} + punycode@2.3.1: {} quansync@0.2.11: {} @@ -5634,6 +5954,36 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -5714,6 +6064,45 @@ snapshots: shebang-regex@3.0.0: {} + shiki@3.21.0: + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/engine-javascript': 3.21.0 + '@shikijs/engine-oniguruma': 3.21.0 + '@shikijs/langs': 3.21.0 + '@shikijs/themes': 3.21.0 + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@4.1.0: {} simple-swizzle@0.2.4: @@ -5726,6 +6115,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + sprintf-js@1.0.3: {} storybook@10.1.10(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -5763,6 +6154,34 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -5816,6 +6235,7 @@ snapshots: dependencies: is-number: 7.0.0 + trim-lines@3.0.1: {} trpc-cli@0.12.2(@trpc/server@11.8.1(typescript@5.9.3))(zod@4.3.5): dependencies: commander: 14.0.2 @@ -5874,6 +6294,29 @@ snapshots: undici-types@6.21.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@2.0.1: {} unplugin@2.3.10: @@ -5901,6 +6344,16 @@ snapshots: uuid@11.1.0: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-plugin-dts@4.5.4(@types/node@22.19.1)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)): dependencies: '@microsoft/api-extractor': 7.55.0(@types/node@22.19.1) @@ -5986,4 +6439,5 @@ snapshots: yocto-queue@0.1.0: {} + zwitch@2.0.4: {} zod@4.3.5: {}