UNPKG

@mentor-apm/react-sortable-tree

Version:

Drag-and-drop sortable component for nested data and hierarchies

269 lines (234 loc) 7.83 kB
const {transformSync} = require('@babel/core') const styleToObject = require('style-to-object') const camelCaseCSS = require('camelcase-css') const uniq = require('lodash.uniq') const {paramCase, toTemplateLiteral} = require('@mdx-js/util') const BabelPluginApplyMdxProp = require('babel-plugin-apply-mdx-type-prop') const BabelPluginExtractImportNames = require('babel-plugin-extract-import-names') // From https://github.com/wooorm/property-information/blob/ca74feb1fcd40753367c75b63c893353cd7d8c70/lib/html.js const spaceSeparatedProperties = [ 'acceptCharset', 'accessKey', 'autoComplete', 'className', 'controlsList', 'headers', 'htmlFor', 'httpEquiv', 'itemProp', 'itemRef', 'itemType', 'ping', 'rel', 'sandbox' ] // eslint-disable-next-line complexity function toJSX(node, parentNode = {}, options = {}) { const { // Default options skipExport = false, preserveNewlines = false, wrapExport } = options let children = '' if (node.properties != null) { // Turn style strings into JSX-friendly style object if (typeof node.properties.style === 'string') { let styleObject = {} styleToObject(node.properties.style, function (name, value) { styleObject[camelCaseCSS(name)] = value }) node.properties.style = styleObject } // Transform class property to JSX-friendly className if (node.properties.class) { node.properties.className = node.properties.class delete node.properties.class } // AriaProperty => aria-property // dataProperty => data-property const paramCaseRe = /^(aria[A-Z])|(data[A-Z])/ node.properties = Object.entries(node.properties).reduce( (properties, [key, value]) => Object.assign({}, properties, { [paramCaseRe.test(key) ? paramCase(key) : key]: value }), {} ) } if (node.type === 'root') { const importNodes = [] const exportNodes = [] const jsxNodes = [] let layout for (const childNode of node.children) { if (childNode.type === 'import') { importNodes.push(childNode) continue } if (childNode.type === 'export') { if (childNode.default) { layout = childNode.value .replace(/^export\s+default\s+/, '') .replace(/;\s*$/, '') continue } exportNodes.push(childNode) continue } jsxNodes.push(childNode) } const exportNames = exportNodes .map(node => node.value.match(/^export\s*(var|const|let|class|function)?\s*(\w+)/) ) .map(match => (Array.isArray(match) ? match[2] : null)) .filter(Boolean) const importStatements = importNodes .map(childNode => toJSX(childNode, node)) .join('\n') const exportStatements = exportNodes .map(childNode => toJSX(childNode, node)) .join('\n') const layoutProps = `const layoutProps = { ${exportNames.join(',\n')} };` const mdxLayout = `const MDXLayout = ${layout ? layout : '"wrapper"'}` const fn = `function MDXContent({ components, ...props }) { return ( <MDXLayout {...layoutProps} {...props} components={components}> ${jsxNodes.map(childNode => toJSX(childNode, node)).join('')} </MDXLayout> ) }; MDXContent.isMDXComponent = true` // Check JSX nodes against imports const babelPluginExtractImportNamesInstance = new BabelPluginExtractImportNames() const filename = options.file && options.file.path transformSync(importStatements, { filename, configFile: false, babelrc: false, plugins: [ require('@babel/plugin-syntax-jsx'), require('@babel/plugin-syntax-object-rest-spread'), babelPluginExtractImportNamesInstance.plugin ] }) const importNames = babelPluginExtractImportNamesInstance.state.names const babelPluginApplyMdxPropInstance = new BabelPluginApplyMdxProp() const babelPluginApplyMdxPropToExportsInstance = new BabelPluginApplyMdxProp() const fnPostMdxTypeProp = transformSync(fn, { filename, configFile: false, babelrc: false, plugins: [ require('@babel/plugin-syntax-jsx'), require('@babel/plugin-syntax-object-rest-spread'), babelPluginApplyMdxPropInstance.plugin ] }).code const exportStatementsPostMdxTypeProps = transformSync(exportStatements, { filename, configFile: false, babelrc: false, plugins: [ require('@babel/plugin-syntax-jsx'), require('@babel/plugin-syntax-object-rest-spread'), babelPluginApplyMdxPropToExportsInstance.plugin ] }).code const allJsxNames = [ ...babelPluginApplyMdxPropInstance.state.names, ...babelPluginApplyMdxPropToExportsInstance.state.names ] const jsxNames = allJsxNames.filter(name => name !== 'MDXLayout') const importExportNames = importNames.concat(exportNames) const shortCodeDef = `const makeShortcode = name => function MDXDefaultShortcode(props) { console.warn("Component " + name + " was not imported, exported, or provided by MDXProvider as global scope") return <div {...props}/> }; ` const fakedModulesForGlobalScope = uniq(jsxNames) .filter(name => !importExportNames.includes(name)) .map(name => `const ${name} = makeShortcode("${name}");`) .join('\n') const fakedModules = (fakedModulesForGlobalScope && [shortCodeDef, fakedModulesForGlobalScope].join('')) || '' const moduleBase = `${importStatements} ${exportStatementsPostMdxTypeProps} ${fakedModules} ${layoutProps} ${mdxLayout}` if (skipExport) { return `${moduleBase} ${fnPostMdxTypeProp}` } if (wrapExport) { return `${moduleBase} ${fnPostMdxTypeProp} export default ${wrapExport}(MDXContent)` } return `${moduleBase} export default ${fnPostMdxTypeProp}` } // Recursively walk through children if (node.children) { children = node.children .map(childNode => { const childOptions = Object.assign({}, options, { // Tell all children inside <pre> tags to preserve newlines as text nodes preserveNewlines: preserveNewlines || node.tagName === 'pre' }) return toJSX(childNode, node, childOptions) }) .join('') } if (node.type === 'element') { let props = '' if (node.properties) { spaceSeparatedProperties.forEach(prop => { if (Array.isArray(node.properties[prop])) { node.properties[prop] = node.properties[prop].join(' ') } }) if (Object.keys(node.properties).length > 0) { props = JSON.stringify(node.properties) } } return `<${node.tagName}${ parentNode.tagName ? ` parentName="${parentNode.tagName}"` : '' }${props ? ` {...${props}}` : ''}>${children}</${node.tagName}>` } // Wraps text nodes inside template string, so that we don't run into escaping issues. if (node.type === 'text') { // Don't wrap newlines unless specifically instructed to by the flag, // to avoid issues like React warnings caused by text nodes in tables. const shouldPreserveNewlines = preserveNewlines || parentNode.tagName === 'p' if (node.value === '\n' && !shouldPreserveNewlines) { return node.value } return toTemplateLiteral(node.value) } if (node.type === 'comment') { return `{/*${node.value}*/}` } if (node.type === 'import' || node.type === 'export' || node.type === 'jsx') { return node.value } } function compile(options = {}) { this.Compiler = function (tree, file) { return toJSX(tree, {}, {file: file || {}, ...options}) } } module.exports = compile exports = compile exports.toJSX = toJSX exports.default = compile