// Convert the saved Figma reference into React Native scene data. No screenshot // is used as a screen: text, fills and controls remain individual native layers. const fs=require('fs'),path=require('path'),parser=require('@babel/parser'); const root=path.resolve(__dirname,'..'); const contexts=JSON.parse(fs.readFileSync(root+'/design/contexts.json')); const native=JSON.parse(fs.readFileSync(root+'/design/native-properties.json')); const api=new Map(native.flatMap(s=>s.layers.map(l=>[l.id,l]))); const xml=fs.readFileSync(root+'/design/metadata.xml','utf8'); const decode=s=>s.replace(/"/g,'"').replace(/'|'/g,"'").replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&'); const nodes={},stack=[]; for(const line of xml.split('\n')){if(/^\s*<\//.test(line)){stack.pop();continue;}const m=line.match(/^\s*<([\w-]+)\s+(.*?)(\/?)>/);if(!m)continue;const a={};for(const [,k,v]of m[2].matchAll(/([\w-]+)="([^"]*)"/g))a[k]=decode(v);const parent=stack.at(-1);const n={id:a.id,name:a.name,t:m[1].toUpperCase(),x:+a.x||0,y:+a.y||0,w:+a.width||0,h:+a.height||0,parent,section:m[1]==='section'?a.name:parent?.section};n.gx=n.x+(parent?.gx||0);n.gy=n.y+(parent?.gy||0);nodes[n.id]=n;if(!m[3])stack.push(n);} function attr(n,key){const a=n.openingElement.attributes.find(a=>a.name?.name===key);return a?.value?.type==='StringLiteral'?a.value.value:a?.value?.expression;} function visit(n,fn){if(!n||typeof n!=='object')return;fn(n);for(const[k,v]of Object.entries(n)){if(['loc','extra'].includes(k))continue;if(Array.isArray(v))v.forEach(a=>visit(a,fn));else if(v&&typeof v==='object')visit(v,fn);}} function value(n){if(!n)return '';if(n.type==='StringLiteral'||n.type==='NumericLiteral')return String(n.value);if(n.type==='TemplateLiteral')return n.quasis.map(q=>q.value.cooked).join('');return '';} function textContent(n){if(n.type==='JSXText'){if(!n.value.trim())return '';return n.value.replace(/\s+/g,' ');}if(n.type==='JSXExpressionContainer')return value(n.expression);if(n.type==='JSXElement'){const tag=n.openingElement.name.name;return (n.children||[]).map(textContent).join('')+(tag==='p'?'\n':tag==='br'?'\n':'');}return '';} function color(s){if(!s)return null;s=s.replace(/_/g,' ');if(s==='white')s='#ffffff';if(s==='black')s='#000000';if(s==='transparent')return {r:0,g:0,b:0,a:0};if(s.startsWith('#')){let h=s.slice(1);if(h.length===3)h=h.split('').map(c=>c+c).join('');return {r:parseInt(h.slice(0,2),16)/255,g:parseInt(h.slice(2,4),16)/255,b:parseInt(h.slice(4,6),16)/255,a:h.length===8?parseInt(h.slice(6),16)/255:1};}const v=s.match(/rgba?\(([^)]+)\)/);if(v){const a=v[1].split(/[,\s]+/).map(Number);return {r:a[0]/255,g:a[1]/255,b:a[2]/255,a:a[3]??1};}return null;} const bracket=(c,key)=>c.match(new RegExp('(?:^| )'+key+'-\\[([^\\]]+)\\]'))?.[1]; const px=(c,key,fallback)=>{const matches=[...c.matchAll(new RegExp('(?:^| )'+key+'-\\[([^\\]]+)\\]','g'))];const v=matches.map(m=>m[1]).find(v=>/^-?[\d.]+px$/.test(v));return v?parseFloat(v):fallback;}; function paint(c,key){const s=bracket(c,key)||c.match(new RegExp('(?:^| )'+key+'-(white|black|transparent)(?: |$)'))?.[1];const col=color(s);return col?[{type:'SOLID',color:col,opacity:1}]:[];} function fillsFromStyle(n){const s=attr(n,'style');if(s?.type!=='ObjectExpression')return [];const bg=s.properties.find(p=>p.key?.name==='backgroundImage')?.value;const str=value(bg);if(!str)return [];const colors=[...str.matchAll(/rgba?\([^)]+\)/g)].map(m=>color(m[0])).filter(Boolean);if(!colors.length)return [];if(colors.every(c=>JSON.stringify(c)===JSON.stringify(colors[0])))return [{type:'SOLID',color:colors[0],opacity:1}];if(str.startsWith('linear-gradient(90deg')&&colors.length===4&&JSON.stringify(colors[0])===JSON.stringify(colors[1]))return [{type:'SOLID',color:colors[0],opacity:1}];return [{type:'GRADIENT_LINEAR',gradientStops:colors.map((color,i)=>({position:i/(colors.length-1),color}))}];} const manifest={},out=[];const errors=[]; for(const [id,code]of Object.entries(contexts)){ const frame=nodes[id];if(!frame){errors.push(id);continue;}const ast=parser.parse(code,{sourceType:'module',plugins:['jsx','typescript']});const urls={};let jsx; visit(ast,n=>{if(n.type==='VariableDeclarator'&&n.init?.type==='StringLiteral')urls[n.id.name]=n.init.value;if(n.type==='JSXElement'&&attr(n,'data-node-id')===id)jsx=n;});if(!jsx){errors.push(id);continue;} const imageCounts={};const scene={id,name:frame.name,w:frame.w,h:frame.h,section:frame.section,canvasX:frame.x,canvasY:frame.y,layers:[]}; function walk(n,nearest=null,ancestors=[],clips=[],opacity=1){if(n.type!=='JSXElement')return;const nid=attr(n,'data-node-id');const cls=attr(n,'className')||'';const meta=nodes[nid];let current=nearest,nextAnc=ancestors,nextClips=clips,nextOpacity=opacity; if(meta){current={id:nid,name:meta.name,t:meta.t,x:meta.gx-frame.gx,y:meta.gy-frame.gy,w:meta.w,h:meta.h,path:ancestors};const original=api.get(nid);const opacityClass=cls.match(/(?:^| )opacity-(\d+)(?: |$)/)?.[1];nextOpacity=opacity*(original?.o??(opacityClass?Number(opacityClass)/100:1));current.o=nextOpacity;const ownDataChildren=[];for(const c of n.children)visit(c,v=>{if(v.type==='JSXElement'&&attr(v,'data-node-id'))ownDataChildren.push(v);});const rawText=textContent(n).replace(/\n$/,'');const isText=meta.t==='TEXT'||(!ownDataChildren.length&&rawText.trim()&&!/m[1])[0]||bracket(cls,'font');const weight=font&&/^\d/.test(font)?+font:cls.includes('font-bold')?700:cls.includes('font-semibold')?600:cls.includes('font-medium')?500:cls.includes('font-light')?300:400; let line;visit(n,v=>{if(v.type==='JSXElement'){const c=attr(v,'className');if(typeof c==='string'&&px(c,'leading',0))line=px(c,'leading');}}); current.runs=original?.runs??[{text:current.text,font:cls.includes('Inter')?'Inter':'SF Pro',size,weight,fill:paint(cls,'text'),line:{unit:'PIXELS',value:line||size*1.2},spacing:{unit:'PIXELS',value:px(cls,'tracking',0)},case:cls.includes('uppercase')?'UPPER':cls.includes('lowercase')?'LOWER':'ORIGINAL',decoration:cls.includes('underline')?'UNDERLINE':'NONE'}];} else{current.f=original?.f??[...paint(cls,'bg'),...fillsFromStyle(n)];current.r=original?.r??px(cls,'rounded',cls.includes('rounded-full')?9999:cls.includes('rounded-')?8:0);current.s=original?.s??paint(cls,'border');current.sw=original?.sw??(current.s.length?1:0);if(original?.e)current.e=original.e;} if(clips.length)current.clips=clips; scene.layers.push(current);nextAnc=[...ancestors,nid];if(/overflow-(clip|hidden)/.test(cls))nextClips=[...clips,{x:current.x,y:current.y,w:current.w,h:current.h,r:current.r||0}];} if(n.openingElement.name.name==='img'&¤t){const src=attr(n,'src');const url=typeof src==='string'?src:urls[src?.name];if(url){const count=imageCounts[current.id]=(imageCounts[current.id]||0)+1;const imageId=current.id+'-image'+(count>1?'-'+count:'');manifest[imageId]={url,format:url.endsWith('.svg')?'svg':'png'};let box={x:current.x,y:current.y,w:current.w,h:current.h};const dimension=(key,total,base)=>{const v=bracket(cls,key);return v?.endsWith('%')?parseFloat(v)*total/100:v?.endsWith('px')?parseFloat(v):base;};box.x+=dimension('left',box.w,0);box.y+=dimension('top',box.h,0);box.w=dimension('w',current.w,box.w);box.h=dimension('h',current.h,box.h);scene.layers.push({id:imageId,name:current.name,t:'IMAGE',...box,path:nextAnc,o:nextOpacity,asset:imageId,clips:url.endsWith('.png')?[...nextClips,{x:current.x,y:current.y,w:current.w,h:current.h,r:current.r||0}]:nextClips});}} for(const c of n.children||[])if(c.type==='JSXElement')walk(c,current,nextAnc,nextClips,nextOpacity); } walk(jsx);for(const layer of scene.layers)if(['VECTOR','BOOLEAN_OPERATION','STAR','POLYGON'].includes(layer.t)&&scene.layers.some(c=>c.asset&&c.path.includes(layer.id))){layer.f=[];layer.s=[];layer.e=[];}out.push(scene); } fs.writeFileSync(root+'/design/scenes.json',JSON.stringify(out));fs.writeFileSync(root+'/design/asset-manifest.json',JSON.stringify(manifest,null,2));console.log({frames:out.length,layers:out.reduce((n,s)=>n+s.layers.length,0),assets:Object.keys(manifest).length,errors});