11import {
22 checkSvgImageChildrenType ,
3+ createInterface ,
34 cssToProps ,
5+ filterPropsByChildrenCountAndType ,
46 fixChildrenText ,
57 formatSvg ,
68 organizeProps ,
@@ -10,7 +12,7 @@ import {
1012} from './utils'
1113import { extractKeyValueFromCssVar } from './utils/extract-key-value-from-css-var'
1214import { textSegmentToTypography } from './utils/text-segment-to-typography'
13- import { toCamel } from './utils/to-camel '
15+ import { toPascal } from './utils/to-pascal '
1416
1517export type ComponentType =
1618 | 'Fragment'
@@ -45,12 +47,15 @@ export class Element {
4547 props ?: Record < string , string >
4648 css ?: Record < string , string >
4749 additionalProps ?: Record < string , string >
50+ parent ?: Element
4851 // for svg
4952 svgVarKeyValue ?: [ string , string ]
5053 componentType ?: ComponentType
5154 skipChildren : boolean = false
52- constructor ( node : SceneNode ) {
55+ assets : Record < string , ( ) => Promise < Uint8Array > > = { }
56+ constructor ( node : SceneNode , parent ?: Element ) {
5357 this . node = node
58+ this . parent = parent
5459 }
5560 async getCss ( ) : Promise < Record < string , string > > {
5661 if ( this . css ) return this . css
@@ -96,19 +101,22 @@ export class Element {
96101 css [ 'padding-right' ]
97102 )
98103 }
99- getImageProps ( ) : Record < string , string > {
104+ getImageProps (
105+ dir : 'icons' | 'images' ,
106+ extension : 'svg' | 'png' ,
107+ ) : Record < string , string > {
100108 return cssToProps (
101109 this . node . parent &&
102110 'width' in this . node . parent &&
103111 this . node . parent . width === this . node . width
104112 ? {
105- src : this . node . name ,
113+ src : `/ ${ dir } / ${ this . node . name } . ${ extension } ` ,
106114 width : '100%' ,
107115 height : '' ,
108116 'aspect-ratio' : `${ Math . floor ( ( this . node . width / this . node . height ) * 100 ) / 100 } ` ,
109117 }
110118 : {
111- src : this . node . name ,
119+ src : `/ ${ dir } / ${ this . node . name } . ${ extension } ` ,
112120 width : this . node . width + 'px' ,
113121 height : this . node . height + 'px' ,
114122 } ,
@@ -148,7 +156,14 @@ export class Element {
148156 break
149157 }
150158 this . componentType = 'Image'
151- Object . assign ( this . additionalProps , this . getImageProps ( ) )
159+ this . addAsset ( this . node , 'svg' )
160+ Object . assign (
161+ this . additionalProps ,
162+ this . getImageProps (
163+ this . node . width !== this . node . height ? 'images' : 'icons' ,
164+ 'svg' ,
165+ ) ,
166+ )
152167 break
153168 }
154169 case 'TEXT' :
@@ -160,7 +175,11 @@ export class Element {
160175 ( this . node . fills as any ) [ 0 ] . type === 'IMAGE'
161176 ) {
162177 this . componentType = 'Image'
163- Object . assign ( this . additionalProps , this . getImageProps ( ) )
178+ Object . assign (
179+ this . additionalProps ,
180+ this . getImageProps ( 'images' , 'png' ) ,
181+ )
182+ this . addAsset ( this . node , 'png' )
164183 }
165184 break
166185 }
@@ -180,16 +199,40 @@ export class Element {
180199 break
181200 const res = await checkSvgImageChildrenType ( this . node )
182201 if ( res ) {
183- if ( res . type === 'SVG' && res . fill ) {
184- this . componentType = 'svg'
185- this . skipChildren = true
186- this . svgVarKeyValue = extractKeyValueFromCssVar ( res . fill )
202+ if ( res . type === 'SVG' && res . fill . size > 0 ) {
203+ if ( res . fill . size === 1 ) {
204+ // mask image
205+ this . componentType = 'Box'
206+ this . skipChildren = true
207+
208+ const props = this . getImageProps ( 'icons' , 'svg' )
209+ props [ 'maskImage' ] = `url(${ props . src } )`
210+ delete props . src
211+ delete props . aspectRatio
212+ Object . assign ( this . additionalProps , {
213+ ...props ,
214+ bg : res . fill . values ( ) . next ( ) . value ,
215+ maskSize : 'contain' ,
216+ maskRepeat : 'no-repeat' ,
217+ } )
218+ this . addAsset ( this . node , 'svg' )
219+ } else {
220+ this . componentType = 'svg'
221+ // render string
222+ this . skipChildren = false
223+
224+ this . addAsset ( this . node , 'svg' )
225+ }
187226 break
188227 }
189228
190229 this . componentType = 'Image'
191230 this . skipChildren = true
192- Object . assign ( this . additionalProps , this . getImageProps ( ) )
231+ Object . assign (
232+ this . additionalProps ,
233+ this . getImageProps ( 'icons' , 'svg' ) ,
234+ )
235+ this . addAsset ( this . node , 'svg' )
193236 }
194237 break
195238 }
@@ -211,33 +254,48 @@ export class Element {
211254 if ( this . node . type === 'TEXT' )
212255 return this . node . characters ? [ this . node . characters ] : [ ]
213256 if ( ! ( 'children' in this . node ) ) return [ ]
214- return this . node . children . map ( ( node ) => new Element ( node ) )
257+ return this . node . children . map ( ( node ) => new Element ( node , this ) )
258+ }
259+
260+ async getAssets ( ) : Promise < Record < string , ( ) => Promise < Uint8Array > > > {
261+ await this . render ( )
262+ return this . assets
263+ }
264+ addAsset ( node : SceneNode , type : 'svg' | 'png' ) {
265+ if ( this . parent ) this . parent . addAsset ( node , type )
266+ else
267+ this . assets [ node . name + '.' + type ] = ( ) =>
268+ node . exportAsync ( {
269+ format : type === 'svg' ? 'SVG' : 'PNG' ,
270+ } )
215271 }
216272
217273 async render ( dep : number = 0 ) : Promise < string > {
218274 if ( ! this . node . visible ) return ''
275+
276+ if ( this . node . type === 'INSTANCE' ) {
277+ return space ( dep ) + `<${ toPascal ( this . node . name ) } />`
278+ }
279+ if ( this . node . type === 'COMPONENT_SET' ) {
280+ return (
281+ await Promise . all (
282+ this . node . children
283+ . map ( ( child ) => new Element ( child , this ) )
284+ . map ( ( child ) => child . render ( dep ) ) ,
285+ )
286+ ) . join ( '\n' )
287+ }
288+
219289 const componentType = await this . getComponentType ( )
220290
221291 if ( componentType === 'svg' ) {
222- // prue svg
223- let value = (
292+ // prue svg
293+ const value = (
224294 await this . node . exportAsync ( {
225295 format : 'SVG_STRING' ,
226296 } )
227297 ) . toString ( )
228298
229- if ( this . svgVarKeyValue ) {
230- value = value . replaceAll ( this . svgVarKeyValue [ 1 ] , 'currentColor' )
231- if ( this . svgVarKeyValue [ 0 ] . startsWith ( '$' ) )
232- this . svgVarKeyValue [ 0 ] =
233- '$' + toCamel ( this . svgVarKeyValue [ 0 ] . slice ( 1 ) )
234-
235- value = value . replace (
236- '<svg' ,
237- `<svg className={css({ color: "${ this . svgVarKeyValue [ 0 ] } " })}` ,
238- )
239- }
240-
241299 return formatSvg ( value , dep )
242300 }
243301
@@ -246,8 +304,15 @@ export class Element {
246304 if ( 'error' in originProps )
247305 return `<${ componentType } error="${ originProps . error } " />`
248306
249- const mergedProps = { ...originProps , ...this . additionalProps }
250307 const children = this . getChildren ( )
308+ const mergedProps = filterPropsByChildrenCountAndType (
309+ children . length ,
310+ componentType ,
311+ {
312+ ...originProps ,
313+ ...this . additionalProps ,
314+ } ,
315+ )
251316
252317 if ( this . node . type === 'TEXT' ) {
253318 const segs = this . node . getStyledTextSegments ( SEGMENT_TYPE )
@@ -329,6 +394,22 @@ export class Element {
329394 const propsString = Object . entries ( props )
330395 . map ( ( [ key , value ] ) => `${ key } ="${ value } "` )
331396 . join ( ' ' )
332- return `${ space ( dep ) } <${ componentType } ${ propsString ? ' ' + propsString : '' } ${ hasChildren ? '' : ' /' } >${ hasChildren ? `\n${ space ( dep + 1 ) } ${ renderChildren } \n` : '' } ${ hasChildren ? `${ space ( dep ) } </${ componentType } >` : '' } `
397+ const body = `${ space ( dep ) } <${ componentType } ${ propsString ? ' ' + propsString : '' } ${ hasChildren ? '' : ' /' } >${ hasChildren ? `\n${ space ( dep + 1 ) } ${ renderChildren } \n` : '' } ${ hasChildren ? `${ space ( dep ) } </${ componentType } >` : '' } `
398+ if ( this . node . type === 'COMPONENT' ) {
399+ const componentName = toPascal ( this . node . name )
400+ const interfaceDecl = createInterface (
401+ componentName ,
402+ this . node . variantProperties ,
403+ )
404+ return `${ interfaceDecl ? interfaceDecl + '\n' : '' } ${ space ( dep ) } export function ${ componentName } (${ interfaceDecl ? `props: ${ componentName } Props` : '' } ) {
405+ return (
406+ ${ body
407+ . split ( '\n' )
408+ . map ( ( line ) => space ( dep + 2 ) + line )
409+ . join ( '\n' ) }
410+ )
411+ }`
412+ }
413+ return body
333414 }
334415}
0 commit comments