UNPKG

@builder.io/mitosis

Version:

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

333 lines (332 loc) 14.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.blockToReact = void 0; const event_handlers_1 = require("../../helpers/event-handlers"); const filter_empty_text_nodes_1 = require("../../helpers/filter-empty-text-nodes"); const is_children_1 = __importDefault(require("../../helpers/is-children")); const is_root_text_node_1 = require("../../helpers/is-root-text-node"); const is_valid_attribute_name_1 = require("../../helpers/is-valid-attribute-name"); const for_1 = require("../../helpers/nodes/for"); const slots_1 = require("../../helpers/slots"); const mitosis_node_1 = require("../../types/mitosis-node"); const html_tags_1 = require("../../constants/html_tags"); const helpers_1 = require("./helpers"); const state_1 = require("./helpers/state"); const NODE_MAPPERS = { Slot(json, options, component, _insideJsx, parentSlots) { var _a, _b; const slotName = ((_a = json.bindings.name) === null || _a === void 0 ? void 0 : _a.code) || json.properties.name; const hasChildren = json.children.length; const renderChildren = () => { var _a; const childrenStr = (_a = json.children) === null || _a === void 0 ? void 0 : _a.map((item) => (0, exports.blockToReact)(item, options, component, true)).join('\n').trim(); /** * Ad-hoc way of figuring out if the children defaultProp is: * - a JSX element, e.g. `<div>foo</div>` * - a JS expression, e.g. `true`, `false` * - a string, e.g. `'Default text'` * * and correctly wrapping it in quotes when appropriate. */ if (childrenStr.startsWith(`<`) && childrenStr.endsWith(`>`)) { return childrenStr; } else if (['false', 'true', 'null', 'undefined'].includes(childrenStr)) { return childrenStr; } else { return `"${childrenStr}"`; } }; if (!slotName) { // TODO: update MitosisNode for simple code const key = Object.keys(json.bindings).find(Boolean); if (key && parentSlots) { parentSlots.push({ key, value: (_b = json.bindings[key]) === null || _b === void 0 ? void 0 : _b.code }); return ''; } const children = (0, helpers_1.processBinding)('props.children', options); return `<>{${children} ${hasChildren ? `|| (${renderChildren()})` : ''}}</>`; } let slotProp = (0, helpers_1.processBinding)(slotName, options).replace('name=', ''); if (!slotProp.startsWith('props.')) { slotProp = `props.${slotProp}`; } return `<>{${slotProp} ${hasChildren ? `|| (${renderChildren()})` : ''}}</>`; }, Fragment(json, options, component) { const wrap = (0, helpers_1.isFragmentWithKey)(json) || (0, helpers_1.wrapInFragment)(json) || (0, is_root_text_node_1.isRootTextNode)(json); return `${wrap ? (0, helpers_1.getFragment)('open', options, json) : ''}${json.children .map((item) => (0, exports.blockToReact)(item, options, component, wrap)) .join('\n')}${wrap ? (0, helpers_1.getFragment)('close', options, json) : ''}`; }, For(_json, options, component, insideJsx) { var _a; const json = _json; const wrap = (0, helpers_1.wrapInFragment)(json); const forArguments = (0, for_1.getForArguments)(json).join(', '); const expression = `${(0, helpers_1.processBinding)((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code, options)}?.map((${forArguments}) => ( ${wrap ? (0, helpers_1.openFrag)(options) : ''}${json.children .filter(filter_empty_text_nodes_1.filterEmptyTextNodes) .map((item) => (0, exports.blockToReact)(item, options, component, wrap)) .join('\n')}${wrap ? (0, helpers_1.closeFrag)(options) : ''} ))`; if (insideJsx) { return `{${expression}}`; } else { return expression; } }, Show(json, options, component, insideJsx) { var _a; const wrap = (0, helpers_1.wrapInFragment)(json) || (0, is_root_text_node_1.isRootTextNode)(json) || component.children[0] === json || // when `<Show><For>...</For></Show>`, we need to wrap the For generated code in a fragment // since it's a `.map()` call (json.children.length === 1 && ['For', 'Show'].includes(json.children[0].name)); const wrapElse = !!(json.meta.else && ((0, helpers_1.wrapInFragment)(json.meta.else) || (0, mitosis_node_1.checkIsForNode)(json.meta.else))); const expression = `${(0, helpers_1.processBinding)((_a = json.bindings.when) === null || _a === void 0 ? void 0 : _a.code, options)} ? ( ${wrap ? (0, helpers_1.openFrag)(options) : ''}${json.children .filter(filter_empty_text_nodes_1.filterEmptyTextNodes) .map((item) => (0, exports.blockToReact)(item, options, component, wrap)) .join('\n')}${wrap ? (0, helpers_1.closeFrag)(options) : ''} ) : ${!json.meta.else ? 'null' : (wrapElse ? (0, helpers_1.openFrag)(options) : '') + (0, exports.blockToReact)(json.meta.else, options, component, wrapElse) + (wrapElse ? (0, helpers_1.closeFrag)(options) : '')}`; if (insideJsx) { return `{${expression}}`; } else { return expression; } }, }; const ATTTRIBUTE_MAPPERS = { spellcheck: 'spellCheck', autocapitalize: 'autoCapitalize', autocomplete: 'autoComplete', for: 'htmlFor', }; // TODO: Maybe in the future allow defining `string | function` as values const BINDING_MAPPERS = { ref(ref, value, options) { if (options === null || options === void 0 ? void 0 : options.preact) { return [ref, value]; } const regexp = /(.+)?props\.(.+)( |\)|;|\()?$/m; if (regexp.test(value)) { const match = regexp.exec(value); const prop = match === null || match === void 0 ? void 0 : match[2]; if (prop) { return [ref, prop]; } } return [ref, value]; }, innerHTML(_key, value, _options, isProperty) { const wrapChar = isProperty ? '"' : ''; let useValue = value.replace(/\s+/g, ' '); if (isProperty) { useValue = value.replace(/"/g, '\\"'); } return ['dangerouslySetInnerHTML', `{__html: ${wrapChar}${useValue}${wrapChar}}`]; }, ...ATTTRIBUTE_MAPPERS, }; const NATIVE_EVENT_MAPPER = { onClick: 'onPress', }; const blockToReact = (json, options, component, insideJsx, parentSlots = []) => { var _a, _b, _c, _d, _e; const needsToRenderSlots = []; if (NODE_MAPPERS[json.name]) { return NODE_MAPPERS[json.name](json, options, component, insideJsx, parentSlots); } if (json.properties._text) { const text = json.properties._text; if (options.type === 'native' && text.trim().length) { return `<Text>${text}</Text>`; } return text; } if ((_a = json.bindings._text) === null || _a === void 0 ? void 0 : _a.code) { const processed = (0, helpers_1.processBinding)(json.bindings._text.code, options); if (options.type === 'native' && !(0, is_children_1.default)({ node: json }) && !(0, slots_1.isSlotProperty)(json.bindings._text.code.split('.')[1] || '')) { return `<Text>{${processed}}</Text>`; } return `{${processed}}`; } let str = ''; str += `<${json.name} `; for (const key in json.properties) { const value = (json.properties[key] || '').replace(/"/g, '&quot;').replace(/\n/g, '\\n'); // Handle src for Image // Handle src for Image if (json.name === 'Image' && key === 'src') { let src; const imageSource = json.properties.src; if (imageSource) { const isUrl = /^(http|https):\/\/[^ "]+$/.test(imageSource); src = isUrl ? `{ uri: '${imageSource}' }` : `require('${imageSource}')`; str += `source={${src}} `; continue; // Skip further processing for 'src' in Image } } // Handle href for TouchableOpacity if (json.name === 'TouchableOpacity' && key === 'href') { const hrefValue = (0, helpers_1.processBinding)(value, options); if (hrefValue) { const onPress = `() => Linking.openURL(${JSON.stringify(hrefValue)})`; str += ` onPress={${onPress}} `; } continue; // Skip further processing for 'href' in TouchableOpacity } // Ignore target for TouchableOpacity if (json.name === 'TouchableOpacity' && key === 'target') { continue; // Skip further processing for 'target' in TouchableOpacity } if (key === 'class') { str = `${str.trim()} className="${value}" `; } else if (BINDING_MAPPERS[key]) { const mapper = BINDING_MAPPERS[key]; if (typeof mapper === 'function') { const [newKey, newValue] = mapper(key, value, options, true); str += ` ${newKey}={${newValue}} `; } else { str += ` ${BINDING_MAPPERS[key]}="${value}" `; } } else { if ((0, is_valid_attribute_name_1.isValidAttributeName)(key)) { str += ` ${key}="${value.replace(/"/g, '&quot;')}" `; } } } for (const key in json.bindings) { // ignore duplicate slot attribute if ((_b = json.slots) === null || _b === void 0 ? void 0 : _b[key]) { continue; } const value = String((_c = json.bindings[key]) === null || _c === void 0 ? void 0 : _c.code); if (key === 'css' && value.trim() === '{}') { continue; } const useBindingValue = (0, helpers_1.processBinding)(value, options); if (json.name === 'Image' && key === 'src') { let src; const imageSource = (0, helpers_1.processBinding)(value, options); if (imageSource) { src = `{ uri: ${imageSource} }`; str += `source={${src}} `; continue; // Skip further processing for 'src' in Image } } // Handle href for TouchableOpacity if (json.name === 'TouchableOpacity' && key === 'href') { const hrefValue = (0, helpers_1.processBinding)(value, options); if (hrefValue) { const onPress = `() => Linking.openURL(${hrefValue})`; str += ` onPress={${onPress}} `; continue; // Skip further processing for 'href' in TouchableOpacity } } // Ignore target for TouchableOpacity if (json.name === 'TouchableOpacity' && key === 'target') { continue; // Skip further processing for 'target' in TouchableOpacity } if (((_d = json.bindings[key]) === null || _d === void 0 ? void 0 : _d.type) === 'spread') { str += ` {...(${value})} `; } else if ((0, event_handlers_1.checkIsEvent)(key)) { const asyncKeyword = ((_e = json.bindings[key]) === null || _e === void 0 ? void 0 : _e.async) ? 'async ' : ''; const { arguments: cusArgs = ['event'] } = json.bindings[key]; const eventName = options.type === 'native' ? NATIVE_EVENT_MAPPER[key] || key : key; str += ` ${eventName}={${asyncKeyword}(${cusArgs.join(',')}) => ${(0, state_1.updateStateSettersInCode)(useBindingValue, options)} } `; } else if (key.startsWith('slot')) { // <Component slotProjected={<AnotherComponent />} /> str += ` ${key}={${value}} `; } else if (key === 'class') { str += ` className={${useBindingValue}} `; } else if (BINDING_MAPPERS[key]) { const mapper = BINDING_MAPPERS[key]; if (typeof mapper === 'function') { const [newKey, newValue] = mapper(key, useBindingValue, options); str += ` ${newKey}={${newValue}} `; } else { if (useBindingValue === 'true') { str += ` ${BINDING_MAPPERS[key]} `; } else { str += ` ${BINDING_MAPPERS[key]}={${useBindingValue}} `; } } } else if (key === 'style' && options.type === 'native' && json.name === 'ScrollView') { // React Native's ScrollView has a different prop for styles: `contentContainerStyle` str += ` contentContainerStyle={${useBindingValue}} `; } else { if ((0, is_valid_attribute_name_1.isValidAttributeName)(key)) { if (useBindingValue === 'true') { str += ` ${key} `; } else { str += ` ${key}={${useBindingValue}} `; } } } } if (json.slots) { for (const key in json.slots) { const value = json.slots[key]; if (!(value === null || value === void 0 ? void 0 : value.length)) { continue; } const reactComponents = value.map((node) => (0, exports.blockToReact)(node, options, component, true)); const slotStringValue = reactComponents.length === 1 ? reactComponents[0] : `<>${reactComponents.join('\n')}</>`; str += ` ${key}={${slotStringValue}} `; } } if (html_tags_1.SELF_CLOSING_HTML_TAGS.has(json.name)) { return str + ' />'; } // Self close by default if no children if (!json.children.length) { str += ' />'; return str; } // TODO: update MitosisNode for simple code let childrenNodes = ''; if (json.children) { childrenNodes = json.children .map((item) => (0, exports.blockToReact)(item, options, component, true, needsToRenderSlots)) .join(''); } if (needsToRenderSlots.length) { needsToRenderSlots.forEach(({ key, value }) => { str += ` ${key}={${value}} `; }); } str = str.trim() + '>'; if (json.children) { str += childrenNodes; } return str + `</${json.name}>`; }; exports.blockToReact = blockToReact;