Last active
December 10, 2024 20:31
-
-
Save kldkv/fd653fc2bdc26c869cadc26ea69dc10f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const babelParser = require("@babel/parser"); | |
const babelCore = require("@babel/core"); | |
const traverse = require("@babel/traverse").default; | |
const generate = require("@babel/generator").default; | |
function generateMemberExpressionString(node) { | |
if (node.type === "MemberExpression") { | |
const object = generateMemberExpressionString(node.object); | |
const property = node.property.name; | |
return `${object}.${property}`; | |
} | |
return node.name; | |
} | |
function renderShowElement(expression) { | |
const memberExpression = generateMemberExpressionString(expression.test); | |
const isNegativeExpression = | |
expression.test.type === "UnaryExpression" && | |
expression.test.operator === "!"; | |
const conditionVariable = isNegativeExpression | |
? expression.test.argument.name | |
: expression.test.name; | |
const alternate = getAttributeValue( | |
isNegativeExpression ? expression.consequent : expression.alternate, | |
); | |
const consequent = getAttributeValue( | |
isNegativeExpression ? expression.alternate : expression.consequent, | |
); | |
return { | |
component: "Show", | |
children: consequent, | |
props: { | |
when: `{{${conditionVariable ?? memberExpression}}}`, | |
fallback: alternate, | |
}, | |
}; | |
} | |
function renderJSXElement(node) { | |
const props = {}; | |
const children = []; | |
const componentName = node.openingElement.name.name; | |
// Получаем свойства компонента | |
node.openingElement.attributes.forEach((attr) => { | |
if (attr.type === "JSXAttribute") { | |
if (["JSXExpressionContainer"].includes(attr.value?.type ?? "")) { | |
props[attr.name.name] = getAttributeValue(attr.value.expression); | |
} else if (attr.value === null) { | |
props[attr.name.name] = true; | |
} else { | |
props[attr.name.name] = attr.value.value; | |
} | |
} | |
}); | |
// Получаем дочерние элементы | |
node.children.forEach((child) => { | |
if (["JSXExpressionContainer"].includes(child.type)) { | |
if (["ConditionalExpression"].includes(child.expression.type)) { | |
children.push(renderShowElement(child.expression)); | |
} | |
} | |
if (child.type === "JSXText") { | |
const trimmedValue = child.value.trim(); | |
if (trimmedValue === "") { | |
return; | |
} | |
children.push(trimmedValue); | |
} else if (child.type === "JSXElement") { | |
children.push(renderJSXElement(child)); | |
} | |
}); | |
const result = { | |
component: componentName, | |
}; | |
if (children.length > 0) { | |
result.children = children; | |
} | |
if ( | |
Object.entries(props).filter(([key, value]) => value !== undefined).length > | |
0 | |
) { | |
result.props = props; | |
} | |
return result; | |
} | |
function getAttributeValue(expression) { | |
if (expression === null) { | |
return true; | |
} | |
if ( | |
[ | |
"StringLiteral", | |
"BooleanLiteral", | |
"BigIntLiteral", | |
"NumericLiteral", | |
"Literal", | |
].includes(expression.type) | |
) { | |
return expression.value; | |
} | |
if (["JSXExpressionContainer"].includes(expression.type)) { | |
return getAttributeValue(expression.expression); | |
} | |
if (["ArrayExpression"].includes(expression.type)) { | |
return expression.elements.map((element) => getAttributeValue(element)); | |
} | |
if (["TemplateLiteral"].includes(expression.type)) { | |
const expressions = expression.expressions.map((element) => ({ | |
...element, | |
value: { | |
raw: element.value, | |
cooked: getAttributeValue(element), | |
}, | |
})); | |
return expressions | |
.concat(expression.quasis) | |
.sort((elementA, elementB) => elementA.start - elementB.start) | |
.reduce( | |
(string, element) => `${string}${element.value.cooked.toString()}`, | |
"", | |
); | |
} | |
if (["ObjectExpression"].includes(expression.type)) { | |
return expression.properties | |
.map((property) => { | |
const key = getAttributeValue(property.key); | |
const value = getAttributeValue(property.value); | |
if (key === undefined || value === undefined) { | |
return null; | |
} | |
return { key, value }; | |
}) | |
.filter((property) => property) | |
.reduce((properties, property) => { | |
return { ...properties, [property.key]: property.value }; | |
}, {}); | |
} | |
if (["JSXElement"].includes(expression.type)) { | |
return renderJSXElement(expression); | |
} | |
if (["NullLiteral"].includes(expression.type)) { | |
return null; | |
} | |
if (["Identifier"].includes(expression.type)) { | |
if (expression.name === "undefined") { | |
return undefined; | |
} | |
return expression.name; | |
} | |
throw new SyntaxError(`${expression.type} is not supported`); | |
} | |
function transformNode(node) { | |
if (node.type === "JSXElement") { | |
return renderJSXElement(node); | |
} | |
} | |
// Основная функция для преобразования JSX в JSON | |
function transformJSXToJson(jsx) { | |
const ast = babelParser.parse(jsx, { | |
sourceType: "module", | |
plugins: ["jsx"], | |
}); | |
let jsonResult; | |
traverse(ast, { | |
JSXElement(path) { | |
jsonResult = transformNode(path.node); | |
path.stop(); // Остановить обход после первого найденного элемента | |
}, | |
}); | |
return jsonResult; | |
} | |
function transformJsonToJSX(json) { | |
const t = babelCore.types; | |
const babe = babelCore; | |
function createJSXElement(node) { | |
if (typeof node === "string") { | |
return t.jsxText(node); | |
} | |
const { component, props = {}, children } = node; | |
// Создаем JSX идентификатор для компонента | |
const jsxIdentifier = t.jsxIdentifier(component); | |
// Преобразуем props в JSX атрибуты | |
const attributes = Object.entries(props) | |
.map(([key, value]) => { | |
const nameIdentifier = t.jsxIdentifier(key); | |
// Обработка динамических значений в фигурных скобках | |
if ( | |
typeof value === "string" && | |
value.startsWith("{{") && | |
value.endsWith("}}") | |
) { | |
const expression = babelCore.parse(value.slice(2, -2)).program.body[0] | |
.expression; | |
return t.jsxAttribute( | |
nameIdentifier, | |
t.jsxExpressionContainer(expression), | |
); | |
} | |
// Обработка булевых значений | |
if (typeof value === "boolean") { | |
return value ? t.jsxAttribute(nameIdentifier) : null; | |
} | |
// Обработка строковых значений | |
if (typeof value === "string") { | |
return t.jsxAttribute(nameIdentifier, t.stringLiteral(value)); | |
} | |
// Обработка объектов | |
if (typeof value === "object" && value !== null) { | |
return t.jsxAttribute( | |
nameIdentifier, | |
t.jsxExpressionContainer(t.valueToNode(value)), | |
); | |
} | |
return t.jsxAttribute( | |
nameIdentifier, | |
t.jsxExpressionContainer(t.valueToNode(value)), | |
); | |
}) | |
.filter(Boolean); | |
// Особая обработка компонента Show | |
if (component === "Show") { | |
const when = props.when.slice(2, -2); | |
const condition = babelCore.parse(when).program.body[0].expression; | |
const consequent = createJSXElement(children); | |
const alternate = props.fallback | |
? createJSXElement(props.fallback) | |
: null; | |
return t.jsxExpressionContainer( | |
t.conditionalExpression( | |
condition, | |
consequent, | |
alternate ?? t.nullLiteral(), | |
), | |
); | |
} | |
// Обработка дочерних элементов | |
const childElements = Array.isArray(children) | |
? children.map((child) => createJSXElement(child)) | |
: children | |
? [createJSXElement(children)] | |
: []; | |
// Создаем JSX элемент | |
return t.jsxElement( | |
t.jsxOpeningElement(jsxIdentifier, attributes, false), | |
t.jsxClosingElement(jsxIdentifier), | |
childElements, | |
false, | |
); | |
} | |
// Создаем AST | |
const ast = babelCore.types.program([ | |
babelCore.types.exportDefaultDeclaration( | |
babelCore.types.arrowFunctionExpression([], createJSXElement(json)), | |
), | |
]); | |
// Генерируем код | |
const output = babelCore.transformFromAstSync(ast, null, { | |
plugins: ["@babel/plugin-syntax-jsx"], | |
retainLines: true, | |
}); | |
return output.code; | |
} | |
module.exports = { | |
transformJSXToJson, | |
transformJsonToJSX, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment