@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
305 lines (304 loc) • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.blockToSwift = void 0;
const helpers_1 = require("./helpers");
// Helper function to sanitize text content for SwiftUI
const sanitizeTextForSwift = (text) => {
if (!text)
return '""';
// Check if text contains newlines
if (text.includes('\n')) {
// Use triple quotes for multiline strings
return `"""${text}"""`;
}
// Escape double quotes in the text
return `"${text.replace(/"/g, '\\"')}"`;
};
const blockToSwift = ({ json, options, parentComponent, }) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
if (json.properties._text) {
return `Text(${sanitizeTextForSwift(json.properties._text)})`;
}
const tag = json.name;
// For fragments, render children without a wrapper
if (tag === 'Fragment') {
return json.children
.map((child) => (0, exports.blockToSwift)({ json: child, options, parentComponent }))
.join('\n');
}
// Process bindings and properties - use parentComponent here instead of json
const processCode = (0, helpers_1.stripStateAndProps)({ json: parentComponent, options });
// Handle ForEach blocks - use bindings.each pattern like other generators
if ((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code) {
const { collection, itemName, indexName } = (0, helpers_1.getForEachParams)(json, processCode);
const forEachContent = json.children
.map((child) => (0, exports.blockToSwift)({ json: child, options, parentComponent }))
.join('\n');
// Check if the collection is using Array.from({length: X}) pattern
const arrayFromMatch = collection.match(/Array\.from\(\s*\{\s*length:\s*(\d+)\s*\}\s*\)/);
if (arrayFromMatch) {
// Convert to SwiftUI's ForEach with a range
const length = arrayFromMatch[1];
if (indexName) {
// With index
return `ForEach(0..<${length}, id: \\.self) { ${indexName} in
let ${itemName} = ${indexName}
${forEachContent}
}`;
}
else {
// Without index
return `ForEach(0..<${length}, id: \\.self) { ${itemName} in
${forEachContent}
}`;
}
}
else {
// Standard collection-based ForEach
if (indexName) {
return `ForEach(Array(zip(${collection}.indices, ${collection})), id: \\.0) { ${indexName}, ${itemName} in
${forEachContent}
}`;
}
else {
return `ForEach(${collection}, id: \\.self) { ${itemName} in
${forEachContent}
}`;
}
}
}
// Determine the SwiftUI component name
const component = (0, helpers_1.jsxElementToSwiftUIView)(tag);
// Handle children
const hasChildren = json.children && json.children.length > 0;
// Handle event handlers
const eventHandlers = Object.entries(json.bindings)
.filter(([key]) => { var _a; return key.startsWith('on') && ((_a = json.bindings[key]) === null || _a === void 0 ? void 0 : _a.code); })
.map(([key, binding]) => {
const swiftEventName = (0, helpers_1.getEventHandlerName)(key);
return `.${swiftEventName}(${processCode((binding === null || binding === void 0 ? void 0 : binding.code) || '')})`;
});
// Handle data bindings (like bind:value)
const dataBindings = Object.entries(json.bindings)
.filter(([key]) => { var _a; return key.startsWith('bind:') && ((_a = json.bindings[key]) === null || _a === void 0 ? void 0 : _a.code); })
.map(([key, binding]) => {
const bindingType = (0, helpers_1.getBindingType)(key);
const bindingValue = processCode((binding === null || binding === void 0 ? void 0 : binding.code) || '');
return { type: bindingType, value: bindingValue };
});
// Handle style properties
const styleModifiers = [];
if (json.bindings.style) {
// Dynamic styles
styleModifiers.push(`// Dynamic styles not fully implemented`);
styleModifiers.push(`.modifier(/* Dynamic style handling needed here */)"`);
}
else if (json.properties.style) {
// Static styles
try {
const styleObj = JSON.parse(json.properties.style);
styleModifiers.push(...(0, helpers_1.cssToSwiftUIModifiers)(styleObj));
}
catch (e) {
styleModifiers.push(`// Could not parse style: ${json.properties.style}`);
}
}
// Check if we need a ScrollView
const needsScroll = (0, helpers_1.needsScrollView)(json);
// Conditional rendering
let result = '';
if ((_b = json.bindings.if) === null || _b === void 0 ? void 0 : _b.code) {
result += `if ${processCode(json.bindings.if.code)} {\n`;
}
else if ((_c = json.bindings.show) === null || _c === void 0 ? void 0 : _c.code) {
// In SwiftUI we can use opacity for show/hide
styleModifiers.push(`.opacity(${processCode(json.bindings.show.code)} ? 1 : 0)`);
}
// Start building the component
let componentCode = '';
switch (component) {
case 'Text':
// Text components in SwiftUI need their content as a parameter
let textContent = '';
if (json.properties._text) {
textContent = sanitizeTextForSwift(json.properties._text);
}
else if ((_d = json.bindings.innerHTML) === null || _d === void 0 ? void 0 : _d.code) {
// For dynamic content, we'll need to handle it as an expression
textContent = processCode(json.bindings.innerHTML.code);
}
else {
textContent = '""';
}
componentCode = `Text(${textContent})`;
break;
case 'Button':
// Find the action from eventHandlers or create an empty one
let buttonAction = '{}';
const onClickHandler = eventHandlers.find((h) => h.includes('onTapGesture'));
if (onClickHandler) {
buttonAction = onClickHandler.replace('.onTapGesture(', '').replace(')', '');
// Remove this handler from the list since we're using it directly
eventHandlers.splice(eventHandlers.indexOf(onClickHandler), 1);
}
const buttonLabel = hasChildren
? json.children
.map((child) => (0, exports.blockToSwift)({ json: child, options, parentComponent }))
.join('\n')
: `Text("${json.properties._text || 'Button'}")`;
componentCode = `Button(action: { ${buttonAction} }) {\n${buttonLabel}\n}`;
break;
case 'TextField':
// TextField can have either bind:value or direct value binding
let bindingExpression = '';
// First check for explicit bind:value
const textBinding = dataBindings.find((b) => b.type === 'value');
// If not found, check for direct value binding
const directValueBinding = ((_e = json.bindings.value) === null || _e === void 0 ? void 0 : _e.code)
? processCode(json.bindings.value.code)
: null;
if (textBinding) {
// Use the explicit binding from dataBindings
bindingExpression = textBinding.value;
}
else if (directValueBinding) {
// Use direct value binding
bindingExpression = directValueBinding;
}
if (bindingExpression) {
// Convert to SwiftUI binding syntax
bindingExpression = bindingExpression.replace(/self\.(\w+)/g, '$$$1');
// If it still doesn't start with $, add it
if (!bindingExpression.startsWith('$')) {
bindingExpression = `$${bindingExpression}`;
}
componentCode = `TextField("${json.properties.placeholder || ''}", text: ${bindingExpression})`;
}
else {
// No binding found, use constant value
componentCode = `TextField("${json.properties.placeholder || ''}", text: .constant("${json.properties.value || ''}"))`;
}
break;
case 'Image':
// Determine if using system image, URL, or asset
if ((_f = json.properties.src) === null || _f === void 0 ? void 0 : _f.startsWith('system-')) {
// System image
const systemName = json.properties.src.replace('system-', '');
componentCode = `Image(systemName: "${systemName}")`;
}
else if ((_g = json.properties.src) === null || _g === void 0 ? void 0 : _g.startsWith('http')) {
// URL image (requires AsyncImage)
componentCode = `AsyncImage(url: URL(string: "${json.properties.src}")!) { image in
image.resizable()
} placeholder: {
ProgressView()
}`;
}
else {
// Asset image
componentCode = `Image("${json.properties.src || 'placeholder'}")`;
}
if (json.properties.resizeMode) {
componentCode += `.resizable().aspectRatio(contentMode: .${json.properties.resizeMode})`;
}
else if (!((_h = json.properties.src) === null || _h === void 0 ? void 0 : _h.startsWith('http'))) {
// Add resizable for non-async images without specific mode
componentCode += `.resizable().aspectRatio(contentMode: .fit)`;
}
break;
case 'VStack':
case 'HStack':
case 'ZStack':
// Stacks with children
const alignment = json.properties.alignment || 'leading';
const spacing = json.properties.spacing || '8';
componentCode = `${component}(alignment: .${alignment}, spacing: ${spacing}) {\n`;
if (hasChildren) {
componentCode += json.children
.map((child) => {
return (0, exports.blockToSwift)({ json: child, options, parentComponent });
})
.join('\n');
}
componentCode += '\n}';
break;
case 'List':
// Lists in SwiftUI
componentCode = `List {\n`;
if (hasChildren) {
componentCode += json.children
.map((child) => {
return (0, exports.blockToSwift)({ json: child, options, parentComponent });
})
.join('\n');
}
componentCode += '\n}';
break;
case 'Picker':
// Handle select element
// Pickers in SwiftUI need a selection binding, a label, and content
const selectBinding = dataBindings.find((b) => b.type === 'value');
const selectionVar = selectBinding ? selectBinding.value : '.constant("")';
// Create label from the "label" property or a default
const pickerLabel = json.properties.label
? `Text("${json.properties.label}")`
: 'Text("Select")';
// Start building the picker
componentCode = `Picker(selection: Binding(get: { ${selectionVar} }, set: { ${selectionVar} = $0 }), label: { ${pickerLabel} }) {`;
// Add options as children
if (hasChildren) {
json.children.forEach((child) => {
var _a;
// For option elements, extract the value and text
if (((_a = child.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'option') {
const optionValue = child.properties.value || '';
const optionText = child.properties._text || optionValue;
componentCode += `\nText("${optionText}").tag("${optionValue}")`;
}
else {
// Handle non-option children (unusual but possible)
componentCode += `\n${(0, exports.blockToSwift)({ json: child, options, parentComponent })}`;
}
});
}
componentCode += '\n}';
break;
default:
// Custom components or other SwiftUI views
if (hasChildren) {
componentCode = `${component} {\n`;
componentCode += json.children
.map((child) => {
return (0, exports.blockToSwift)({ json: child, options, parentComponent });
})
.join('\n');
componentCode += '\n}';
}
else {
componentCode = `${component}()`;
}
}
// Apply modifiers
styleModifiers.forEach((modifier) => {
componentCode += `\n${modifier}`;
});
// Add event handlers that weren't specifically handled above
eventHandlers
.filter((handler) => !componentCode.includes(handler))
.forEach((handler) => {
componentCode += `\n${handler}`;
});
// Wrap with ScrollView if needed
if (needsScroll) {
componentCode = `ScrollView {\n${componentCode}\n}`;
}
// Close conditional rendering block if needed
if ((_j = json.bindings.if) === null || _j === void 0 ? void 0 : _j.code) {
result += componentCode + '\n}';
}
else {
result += componentCode;
}
return result;
};
exports.blockToSwift = blockToSwift;