@@ -2,8 +2,10 @@ import fs from 'node:fs'
22import path from 'node:path'
33import { compile , Element } from 'stylis'
44
5- export interface ParsedInput {
6- className : string
5+ // Components in a MistCSS file
6+ export type Components = Record < string , Component >
7+
8+ type Component = {
79 tag : string
810 data : Record < string , string [ ] | boolean >
911}
@@ -12,81 +14,106 @@ const enumDataAttributeRegex =
1214 / \[ d a t a - (?< attribute > [ a - z - ] + ) = ' (?< value > [ ^ ' ] * ) ' \] / g
1315const booleanDataAttributeRegex = / \[ d a t a - (?< attribute > [ a - z - ] + ) (? = \] ) / g
1416
15- function visit ( nodes : Element [ ] , arr : { type : string ; props : string [ ] } [ ] ) {
17+ // Visit all nodes in the AST and return @scope and rule nodes
18+ function visit ( nodes : Element [ ] ) : { type : string ; props : string [ ] } [ ] {
19+ let result : { type : string ; props : string [ ] } [ ] = [ ]
20+
1621 for ( const node of nodes ) {
1722 if ( [ '@scope' , 'rule' ] . includes ( node . type ) && Array . isArray ( node . props ) ) {
18- arr . push ( { type : node . type , props : node . props } )
23+ result . push ( { type : node . type , props : node . props } )
1924 }
2025
2126 if ( Array . isArray ( node . children ) ) {
22- visit ( node . children , arr )
27+ result = result . concat ( visit ( node . children ) )
2328 }
2429 }
25- }
2630
27- export function parseInput ( input : string ) : ParsedInput {
28- const result : ParsedInput = { className : '' , tag : '' , data : { } }
31+ return result
32+ }
2933
30- const arr : { type : string ; props : string [ ] } [ ] = [ ]
31- visit ( compile ( input ) , arr )
34+ export function parseInput ( input : string ) : Components {
35+ const components : Components = { }
3236
33- arr . forEach ( ( node ) => {
37+ let name
38+ const nodes = visit ( compile ( input ) )
39+ console . log ( nodes )
40+ for ( const node of nodes ) {
41+ // Parse name
3442 if ( node . type === '@scope' ) {
3543 const prop = node . props [ 0 ]
3644 if ( prop === undefined ) {
37- return
45+ throw new Error ( 'Invalid MistCSS file, no class found in @scope' )
3846 }
39- result . className = prop . replace ( '(.' , '' ) . replace ( ')' , '' )
40- return
47+ name = prop . replace ( '(.' , '' ) . replace ( ')' , '' )
48+ // Convert to PascalCase
49+ name = name . replace ( / (?: ^ | - ) ( [ a - z ] ) / g, ( _ , g ) => g . toUpperCase ( ) )
50+ components [ name ] = { tag : '' , data : { } }
51+ continue
4152 }
4253
54+ // Parse tag and data attributes
4355 if ( node . type === 'rule' ) {
4456 const prop = node . props [ 0 ]
45- if ( prop === undefined ) {
46- return
57+ if ( prop === undefined || name === undefined ) {
58+ continue
59+ }
60+ const component = components [ name ]
61+ if ( component === undefined ) {
62+ continue
4763 }
4864
4965 // Parse tag
5066 if ( prop . endsWith ( ':scope' ) ) {
51- result . tag = prop . replace ( ':scope' , '' )
67+ component . tag = prop . replace ( ':scope' , '' )
68+ continue
5269 }
5370
5471 // Parse enum data attributes
55- for ( const match of prop . matchAll ( enumDataAttributeRegex ) ) {
72+ const enumMatches = prop . matchAll ( enumDataAttributeRegex )
73+ for ( const match of enumMatches ) {
5674 const attribute = match . groups ?. [ 'attribute' ]
5775 const value = match . groups ?. [ 'value' ] ?? ''
5876
5977 if ( attribute === undefined ) {
6078 continue
6179 }
6280
63- result . data [ attribute ] ||= [ ]
81+ // Convert to camelCase
82+ const camelCasedAttribute = attribute . replace (
83+ / - ( [ a - z ] ) / g,
84+ ( g ) => g [ 1 ] ?. toUpperCase ( ) ?? '' ,
85+ )
6486
65- const attr = result . data [ attribute ]
87+ // Initialize data if it doesn't exist
88+ component . data [ camelCasedAttribute ] ||= [ ]
89+ const attr = component . data [ camelCasedAttribute ]
6690 if ( Array . isArray ( attr ) && ! attr . includes ( value ) ) {
6791 attr . push ( value )
6892 }
93+ continue
6994 }
7095
7196 // Parse boolean data attributes
72- for ( const match of prop . matchAll ( booleanDataAttributeRegex ) ) {
97+ const booleanMatches = prop . matchAll ( booleanDataAttributeRegex )
98+ for ( const match of booleanMatches ) {
7399 const attribute = match . groups ?. [ 'attribute' ]
74100 if ( attribute === undefined ) {
75101 continue
76102 }
77103
78- result . data [ attribute ] ||= true
104+ component . data [ attribute ] ||= true
105+ continue
79106 }
80107 }
81- } )
108+ }
82109
83- return result
110+ return components
84111}
85112
86- function renderProps ( parsedInput : ParsedInput ) : string {
87- return Object . keys ( parsedInput . data )
113+ function renderProps ( component : Component ) : string {
114+ return Object . keys ( component . data )
88115 . map ( ( attribute ) => {
89- const values = parsedInput . data [ attribute ]
116+ const values = component . data [ attribute ]
90117 if ( Array . isArray ( values ) ) {
91118 return `${ attribute } ?: ${ values
92119 . map ( ( value ) => `'${ value } '` )
@@ -98,33 +125,45 @@ function renderProps(parsedInput: ParsedInput): string {
98125 . join ( '\n' )
99126}
100127
101- export function render ( name : string , parsedInput : ParsedInput ) : string {
102- return `// Generated by MistCSS, do not modify
103- import './${ name } .mist.css'
104-
105- type Props = {
128+ function renderComponent ( components : Components , name : string ) : string {
129+ const component = components [ name ]
130+ if ( component === undefined ) {
131+ return ''
132+ }
133+ return `type ${ name } Props = {
106134 children?: React.ReactNode
107- ${ renderProps ( parsedInput ) }
108- } & JSX.IntrinsicElements['${ parsedInput . tag } ']
135+ ${ renderProps ( component ) }
136+ } & JSX.IntrinsicElements['${ component . tag } ']
109137
110138export function ${ name } ({ ${ [
111139 'children' ,
112- ...Object . keys ( parsedInput . data ) ,
140+ ...Object . keys ( component . data ) ,
113141 '...props' ,
114- ] . join ( ', ' ) } }: Props) {
142+ ] . join ( ', ' ) } }: ${ name } Props) {
115143 return (
116- <${ parsedInput . tag } {...props} className="${ parsedInput . className } " ${ Object . keys (
117- parsedInput . data ,
144+ <${ component . tag } {...props} className="${ name } " ${ Object . keys (
145+ component . data ,
118146 )
119147 . map ( ( key ) => `data-${ key } ={${ key } }` )
120148 . join ( ' ' ) } >
121149 {children}
122- </${ parsedInput . tag } >
150+ </${ component . tag } >
123151 )
124152}
125153`
126154}
127155
156+ export function render ( name : string , components : Components ) : string {
157+ return `// Generated by MistCSS, do not modify
158+ import './${ name } .mist.css'
159+
160+ ${ Object . keys ( components )
161+ . map ( ( key ) => renderComponent ( components , key ) )
162+ . join ( '\n' )
163+ . trim ( ) }
164+ `
165+ }
166+
128167export function createFile ( filename : string ) {
129168 let data = fs . readFileSync ( filename , 'utf8' )
130169 const parsedInput = parseInput ( data )
0 commit comments