1010 */
1111
1212import type { SandpackFile } from '@codesandbox/sandpack-react/unstyled' ;
13- import type { PropsWithChildren , ReactElement , HTMLAttributes } from 'react' ;
13+ import { Children , isValidElement } from 'react' ;
14+ import type {
15+ PropsWithChildren ,
16+ ReactElement ,
17+ HTMLAttributes ,
18+ ReactNode ,
19+ } from 'react' ;
1420import { getMDXName } from '../getMDXName' ;
1521
1622export const AppJSPath = `/src/App.js` ;
1723export const StylesCSSPath = `/src/styles.css` ;
1824export const SUPPORTED_FILES = [ AppJSPath , StylesCSSPath ] ;
1925
26+ export type SandpackSnippetFile = SandpackFile & {
27+ visible ?: boolean ;
28+ } ;
29+
2030/**
2131 * Tokenize meta attributes while ignoring brace-wrapped metadata (e.g. {expectedErrors: …}).
2232 */
@@ -77,22 +87,105 @@ function splitMeta(meta: string): string[] {
7787 return tokens ;
7888}
7989
90+ function collectCodeSnippets ( children : ReactNode ) : ReactElement [ ] {
91+ const codeSnippets : ReactElement [ ] = [ ] ;
92+
93+ Children . forEach ( children , ( child ) => {
94+ if ( ! isValidElement ( child ) ) {
95+ return ;
96+ }
97+
98+ if ( getMDXName ( child ) === 'pre' ) {
99+ codeSnippets . push ( child ) ;
100+ return ;
101+ }
102+
103+ const props = child . props as { children ?: ReactNode } | null ;
104+ if ( props ?. children != null ) {
105+ codeSnippets . push ( ...collectCodeSnippets ( props . children ) ) ;
106+ }
107+ } ) ;
108+
109+ return codeSnippets ;
110+ }
111+
112+ type CodeElementProps = HTMLAttributes < HTMLDivElement > & {
113+ meta ?: string ;
114+ children ?: ReactNode ;
115+ } ;
116+
117+ function findCodeElement (
118+ children : ReactNode
119+ ) : ReactElement < CodeElementProps > | null {
120+ let codeElement : ReactElement < CodeElementProps > | null = null ;
121+
122+ Children . forEach ( children , ( child ) => {
123+ if ( codeElement || ! isValidElement ( child ) ) {
124+ return ;
125+ }
126+
127+ const props = child . props as CodeElementProps | null ;
128+ if (
129+ getMDXName ( child ) === 'code' ||
130+ typeof props ?. meta === 'string' ||
131+ ( typeof props ?. className === 'string' &&
132+ props . className . startsWith ( 'language-' ) )
133+ ) {
134+ codeElement = child as ReactElement < CodeElementProps > ;
135+ return ;
136+ }
137+
138+ if ( props ?. children != null ) {
139+ codeElement = findCodeElement ( props . children ) ;
140+ }
141+ } ) ;
142+
143+ return codeElement ;
144+ }
145+
146+ function getTextContent ( node : ReactNode ) : string {
147+ let text = '' ;
148+
149+ Children . forEach ( node , ( child ) => {
150+ if ( typeof child === 'string' || typeof child === 'number' ) {
151+ text += child ;
152+ return ;
153+ }
154+
155+ if ( ! isValidElement ( child ) ) {
156+ return ;
157+ }
158+
159+ const props = child . props as { children ?: ReactNode } | null ;
160+ text += getTextContent ( props ?. children ?? null ) ;
161+ } ) ;
162+
163+ return text ;
164+ }
165+
80166export const createFileMap = ( codeSnippets : any ) => {
81- return codeSnippets . reduce (
82- ( result : Record < string , SandpackFile > , codeSnippet : React . ReactElement ) => {
83- if ( getMDXName ( codeSnippet ) !== 'pre' ) {
84- return result ;
167+ return collectCodeSnippets ( codeSnippets ) . reduce (
168+ (
169+ result : Record < string , SandpackSnippetFile > ,
170+ codeSnippet : React . ReactElement
171+ ) => {
172+ const codeElement = findCodeElement (
173+ (
174+ codeSnippet . props as PropsWithChildren < {
175+ children ?: ReactNode ;
176+ } >
177+ ) . children
178+ ) ;
179+
180+ if ( ! codeElement ) {
181+ throw new Error ( 'Code block is missing a code element.' ) ;
85182 }
86- const { props} = (
87- codeSnippet . props as PropsWithChildren < {
88- children : ReactElement <
89- HTMLAttributes < HTMLDivElement > & { meta ?: string }
90- > ;
91- } >
92- ) . children ;
183+
184+ const props = codeElement . props ;
93185 let filePath ; // path in the folder structure
94186 let fileHidden = false ; // if the file is available as a tab
95187 let fileActive = false ; // if the file tab is shown by default
188+ let fileVisible = false ; // if the file tab should be forced visible
96189
97190 if ( props . meta ) {
98191 const tokens = splitMeta ( props . meta ) ;
@@ -108,6 +201,9 @@ export const createFileMap = (codeSnippets: any) => {
108201 if ( tokens . includes ( 'active' ) ) {
109202 fileActive = true ;
110203 }
204+ if ( tokens . includes ( 'visible' ) ) {
205+ fileVisible = true ;
206+ }
111207 } else {
112208 if ( props . className === 'language-js' ) {
113209 filePath = AppJSPath ;
@@ -138,9 +234,10 @@ export const createFileMap = (codeSnippets: any) => {
138234 ) ;
139235 }
140236 result [ filePath ] = {
141- code : ( props . children || '' ) as string ,
237+ code : getTextContent ( props . children ?? null ) ,
142238 hidden : fileHidden ,
143239 active : fileActive ,
240+ visible : fileVisible ,
144241 } ;
145242
146243 return result ;
0 commit comments