Skip to content

Instantly share code, notes, and snippets.

@kldkv
Last active December 10, 2024 20:31
Show Gist options
  • Save kldkv/fd653fc2bdc26c869cadc26ea69dc10f to your computer and use it in GitHub Desktop.
Save kldkv/fd653fc2bdc26c869cadc26ea69dc10f to your computer and use it in GitHub Desktop.
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