UNPKG

@builder.io/mitosis

Version:

Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io

413 lines (412 loc) 16.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertJsFunctionToSwift = exports.extractFunctionSignature = exports.getBindingType = exports.camelToSnakeCase = exports.getForEachParams = exports.isForEachBlock = exports.needsScrollView = exports.getEventHandlerName = exports.getStatePropertyTypeAnnotation = exports.cssToSwiftUIModifiers = exports.jsxElementToSwiftUIView = exports.getSwiftType = exports.stripStateAndProps = exports.ensureSwiftStringFormat = exports.convertConsoleLogToPrint = void 0; const capitalize_1 = require("../../helpers/capitalize"); const strip_state_and_props_refs_1 = require("../../helpers/strip-state-and-props-refs"); // TODO(kyle): use babel here to do ast const convertConsoleLogToPrint = (code) => { if (!code) return code; // Match console.log statements with various argument patterns return code.replace(/console\.log\s*\(\s*(.*?)\s*\)/g, (match, args) => { // Handle empty console.log() if (!args.trim()) { return 'print()'; } // Simple handling for basic console.log calls // For complex cases, we'd need a more sophisticated parser return `print(${(0, exports.ensureSwiftStringFormat)(args)})`; }); }; exports.convertConsoleLogToPrint = convertConsoleLogToPrint; // Helper function to ensure Swift strings use double quotes const ensureSwiftStringFormat = (code) => { if (!code) return code; // We need a more reliable approach to handle nested quotes // This uses a state machine approach to track whether we're inside double quotes let result = ''; let insideDoubleQuotes = false; for (let i = 0; i < code.length; i++) { const char = code[i]; const prevChar = i > 0 ? code[i - 1] : ''; // Handle quote state tracking if (char === '"' && prevChar !== '\\') { insideDoubleQuotes = !insideDoubleQuotes; result += char; } // Only replace single quotes when not inside double quotes else if (char === "'" && prevChar !== '\\' && !insideDoubleQuotes) { // Start of a single-quoted string result += '"'; // Find the end of the single-quoted string, accounting for escaped quotes let j = i + 1; while (j < code.length) { if (code[j] === "'" && code[j - 1] !== '\\') { break; } j++; } // Add the string content result += code.substring(i + 1, j); // Add closing double quote if we found the end if (j < code.length) { result += '"'; i = j; // Skip to the end of the single-quoted string } else { // If no closing quote was found, just add the single quote as is result = result.substring(0, result.length - 1) + "'"; } } else { result += char; } } return result; }; exports.ensureSwiftStringFormat = ensureSwiftStringFormat; const stripStateAndProps = ({ json, options, }) => { return (code) => { // Convert console.log statements to Swift print code = (0, exports.convertConsoleLogToPrint)(code); // Ensure Swift strings use double quotes code = (0, exports.ensureSwiftStringFormat)(code); // In Swift, we use self.propertyName for accessing properties return (0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(code, { includeState: true, includeProps: true, replaceWith: (name) => { // In Swift, we access properties with self.propertyName return `self.${name}`; }, }); }; }; exports.stripStateAndProps = stripStateAndProps; const getSwiftType = (type) => { if (!type) return 'Any'; // Handle array types with proper Swift syntax if (type.includes('Array<') || type.includes('[]') || type.toLowerCase().startsWith('array')) { // Extract the element type from Array<ElementType> let elementType = 'Any'; // Match different array type patterns const arrayMatch = type.match(/Array<([^>]+)>/i) || type.match(/([^[\]]+)\[\]/i) || type.match(/array\s*<([^>]+)>/i); if (arrayMatch && arrayMatch[1]) { elementType = (0, exports.getSwiftType)(arrayMatch[1].trim()); } // Return Swift array type: [ElementType] return `[${elementType}]`; } // Handle primitive types switch (type.toLowerCase()) { case 'string': return 'String'; case 'number': return 'Double'; case 'boolean': case 'bool': return 'Bool'; case 'any': return 'Any'; case 'void': return 'Void'; case 'object': return '[String: Any]'; case 'null': case 'undefined': return 'Optional<Any>'; default: // For complex types, return as is with first letter capitalized return type.charAt(0).toUpperCase() + type.slice(1); } }; exports.getSwiftType = getSwiftType; const jsxElementToSwiftUIView = (tagName) => { // Map JSX/HTML elements to SwiftUI components switch (tagName.toLowerCase()) { case 'div': return 'VStack'; case 'span': case 'p': case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': return 'Text'; case 'img': return 'Image'; case 'input': return 'TextField'; case 'button': return 'Button'; case 'a': return 'Link'; case 'ul': return 'List'; case 'li': return 'Text'; // Will be wrapped in List case 'form': return 'Form'; case 'select': return 'Picker'; case 'option': return 'Text'; // Options in SwiftUI are part of the Picker content default: // For custom components or unrecognized tags return (0, capitalize_1.capitalize)(tagName); } }; exports.jsxElementToSwiftUIView = jsxElementToSwiftUIView; const cssToSwiftUIModifiers = (style) => { const modifiers = []; // Map CSS properties to SwiftUI modifiers Object.entries(style).forEach(([key, value]) => { switch (key) { case 'backgroundColor': modifiers.push(`.background(Color("${value}"))`); break; case 'color': modifiers.push(`.foregroundColor(Color("${value}"))`); break; case 'fontSize': const fontSize = parseInt(value); if (!isNaN(fontSize)) { modifiers.push(`.font(.system(size: ${fontSize}))`); } break; case 'fontWeight': modifiers.push(`.fontWeight(.${value})`); break; case 'padding': modifiers.push(`.padding(${value})`); break; case 'margin': // Swift doesn't have direct margin equivalent, we'll use padding modifiers.push(`// Note: 'margin' converted to padding: ${value}`); modifiers.push(`.padding(${value})`); break; case 'width': modifiers.push(`.frame(width: ${value})`); break; case 'height': modifiers.push(`.frame(height: ${value})`); break; // Add more CSS to SwiftUI modifier mappings as needed default: modifiers.push(`// Unmapped style: ${key}: ${value}`); } }); return modifiers; }; exports.cssToSwiftUIModifiers = cssToSwiftUIModifiers; const getStatePropertyTypeAnnotation = (propertyType, type) => { // Use appropriate SwiftUI property wrappers switch (propertyType) { case 'reactive': // For reactive state, use @State for simple values // @Observable would be used for classes but requires Swift 5.9+/iOS 17+ return `@State private var`; case 'normal': // For normal state, use @State for simple values return `@State private var`; default: // For non-reactive values, use a regular property return `var`; } }; exports.getStatePropertyTypeAnnotation = getStatePropertyTypeAnnotation; const getEventHandlerName = (eventName) => { switch (eventName) { case 'onClick': return 'onTapGesture'; case 'onChange': return 'onChange'; case 'onInput': return 'onEditingChanged'; case 'onBlur': return 'onSubmit'; case 'onFocus': return 'onEditingChanged'; default: return eventName; } }; exports.getEventHandlerName = getEventHandlerName; const needsScrollView = (json) => { // Check if overflow property indicates scrolling if (json.properties.style) { try { const styleObj = JSON.parse(json.properties.style); return (styleObj.overflow === 'auto' || styleObj.overflow === 'scroll' || styleObj.overflowY === 'auto' || styleObj.overflowY === 'scroll' || styleObj.overflowX === 'auto' || styleObj.overflowX === 'scroll'); } catch (e) { // If style can't be parsed, check for overflow directly in the style string const styleStr = json.properties.style; return (styleStr.includes('overflow:auto') || styleStr.includes('overflow:scroll') || styleStr.includes('overflow-y:auto') || styleStr.includes('overflow-y:scroll') || styleStr.includes('overflow-x:auto') || styleStr.includes('overflow-x:scroll')); } } return false; }; exports.needsScrollView = needsScrollView; const isForEachBlock = (json) => { var _a; // Check if this is a ForEach binding using the bindings.each pattern return !!((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code); }; exports.isForEachBlock = isForEachBlock; const getForEachParams = (json, processCode) => { var _a, _b; if (!((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code)) { return { collection: '', itemName: 'item', indexName: null }; } const eachCode = json.bindings.each.code; let itemName = 'item'; let indexName = null; // Extract collection, item name, and index name from each binding try { // Parse expressions like: items.map(item => ...) // or items.map((item, index) => ...) const match = eachCode.match(/(\w+)\.map\(\s*(?:\()?([^,)]+)(?:,\s*([^)]+))?\)?/); if (match) { const collection = processCode(match[1]); itemName = match[2].trim(); indexName = ((_b = match[3]) === null || _b === void 0 ? void 0 : _b.trim()) || null; return { collection, itemName, indexName }; } // Fallback to the whole code as collection if pattern doesn't match return { collection: processCode(eachCode), itemName, indexName, }; } catch (e) { console.warn('Failed to parse each binding:', eachCode); return { collection: processCode(eachCode), itemName, indexName, }; } }; exports.getForEachParams = getForEachParams; const camelToSnakeCase = (str) => { return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); }; exports.camelToSnakeCase = camelToSnakeCase; const getBindingType = (key) => { if (key.startsWith('bind:')) { return key.substring(5); } return key; }; exports.getBindingType = getBindingType; /** * Extract function signature information from JavaScript function code */ const extractFunctionSignature = (code) => { // Default values let name = ''; let params = []; let returnType = 'Void'; let body = ''; // Extract function name, parameters, and body const funcMatch = code.match(/(?:function\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)?\s*\(([^)]*)\)\s*(?:=>)?\s*(?:{([\s\S]*)}|(.*))/); if (funcMatch) { name = funcMatch[1] || ''; // Extract parameters const paramsStr = funcMatch[2].trim(); if (paramsStr) { params = paramsStr.split(',').map((param) => { // Handle TypeScript-style parameter types if present const paramParts = param.trim().split(':'); const paramName = paramParts[0].trim(); const paramType = paramParts.length > 1 ? (0, exports.getSwiftType)(paramParts[1].trim()) : 'Any'; return { name: paramName, type: paramType }; }); } // Extract function body body = funcMatch[3] || funcMatch[4] || ''; // Try to determine return type from TypeScript annotations or infer from return statements const returnTypeMatch = code.match(/\)\s*:\s*([^{]+)/); if (returnTypeMatch) { returnType = (0, exports.getSwiftType)(returnTypeMatch[1].trim()); } else if (body.includes('return')) { // Try to infer from return statements const returnValueMatch = body.match(/return\s+(["'].*["']|true|false|\d+|\d+\.\d+|\[.*\])/); if (returnValueMatch) { const returnValue = returnValueMatch[1]; if (returnValue.startsWith('"') || returnValue.startsWith("'")) { returnType = 'String'; } else if (returnValue === 'true' || returnValue === 'false') { returnType = 'Bool'; } else if (returnValue.match(/^\d+$/)) { returnType = 'Int'; } else if (returnValue.match(/^\d+\.\d+$/)) { returnType = 'Double'; } else if (returnValue.startsWith('[')) { returnType = '[Any]'; } } } } return { name, params, returnType, body }; }; exports.extractFunctionSignature = extractFunctionSignature; /** * Convert JavaScript function code to Swift function syntax */ const convertJsFunctionToSwift = (code, functionName) => { // Extract the function signature const { name, params, returnType, body } = (0, exports.extractFunctionSignature)(code); // Use provided name or extracted name const finalName = functionName || name || 'function'; // Convert function body to Swift let swiftBody = body // Convert variable declarations .replace(/\bvar\s+(\w+)/g, 'var $1') .replace(/\blet\s+(\w+)/g, 'let $1') .replace(/\bconst\s+(\w+)/g, 'let $1') // Convert common array methods .replace(/\.push\(/g, '.append(') .replace(/\.map\(/g, '.map(') .replace(/\.filter\(/g, '.filter(') .replace(/\.includes\(/g, '.contains(') .replace(/\.indexOf\(/g, '.firstIndex(of: ') // Convert null/undefined checks .replace(/=== null/g, '== nil') .replace(/!== null/g, '!= nil') .replace(/=== undefined/g, '== nil') .replace(/!== undefined/g, '!= nil') // Convert console.log .replace(/console\.log\((.+?)\)/g, 'print($1)'); // Create parameter list with Swift types const paramList = params.map((p) => `${p.name}: ${p.type}`).join(', '); // Build the Swift function signature const signature = `func ${finalName}(${paramList}) -> ${returnType}`; // Build the complete Swift function const swiftCode = `${signature} {\n ${swiftBody}\n}`; return { swiftCode, signature }; }; exports.convertJsFunctionToSwift = convertJsFunctionToSwift;