Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save christian-bromann/545df090961c57adcc71f3b2b9e6e8da to your computer and use it in GitHub Desktop.
Save christian-bromann/545df090961c57adcc71f3b2b9e6e8da to your computer and use it in GitHub Desktop.
import decamelize from 'decamelize';
import { namedTypes } from 'ast-types';
import { possibleStandardNames } from './constants.js';
type Node = namedTypes.Property | namedTypes.ObjectProperty | namedTypes.Expression | namedTypes.SpreadElement | namedTypes.SpreadProperty | namedTypes.ObjectMethod | undefined
/**
* Get the React property name for a given Stencil property name
* @param propName - The Stencil property name
* @returns The React property name
*/
export function getReactPropertyName (propName: string) {
return (
/**
* Either use a known standard name
*/
possibleStandardNames[propName as keyof typeof possibleStandardNames] ||
/**
* or use the original name but transform camelCase to kebab-case as
* Stencil only supports kebab-casing using HTML templates.
*/
propName.replace(/([A-Z])/g, '-$1').toLowerCase()
)
}
/**
* Parses a React AST into HTML
* @param {Object} ast - The AST object to parse
* @return {string} The resulting HTML
*/
export function parseAstToHtml(ast: Node) {
if (!ast) return '';
// Handle the root node
if (namedTypes.Property.check(ast) && namedTypes.Identifier.check(ast.key) && ast.key.name === 'children') {
if (namedTypes.ArrayExpression.check(ast.value)) {
return parseChildren(ast.value.elements.filter(Boolean) as Node[]);
}
return parseNode(ast.value);
}
// Handle other types of nodes
return parseNode(ast);
}
/**
* Parse an array of child elements
* @param {Array} elements - Array of child elements
* @return {string} Concatenated HTML string
*/
function parseChildren(elements: Node[]) {
return elements.map(element => parseNode(element)).join('\n');
}
/**
* Parse a single node from the AST
* @param {Object} node - The node to parse
* @return {string} HTML representation of the node
*/
function parseNode(node: Node) {
const callNode = namedTypes.CallExpression.check(node)
? node as namedTypes.CallExpression
: undefined;
// Handle CallExpression which represents a React component (via _jsxDEV calls)
if (
callNode &&
namedTypes.Identifier.check(callNode.callee) &&
callNode.callee.name.includes('jsxDEV') &&
callNode.arguments.length >= 2
) {
// Get the component name and props
const componentName = extractComponentName(callNode.arguments[0]);
const props = extractProps(callNode.arguments[1]);
// Handle self-closing tags
if (!props.children) {
return `<${componentName} ${formatProps(props)} />`;
}
// Handle tags with content
return `<${componentName} ${formatProps(props)}>${props.children}</${componentName}>`;
}
const literalNode = namedTypes.Literal.check(node)
? node as namedTypes.Literal
: undefined;
if (literalNode) {
return literalNode.value;
}
console.log('NOX', node);
// Handle other node types or return empty string if not recognized
return '';
}
/**
* Extract component name from the component argument
* @param {Object} componentArg - The component argument from _jsxDEV
* @return {string} The component name
*/
function extractComponentName(componentArg: Node) {
const identifier = namedTypes.Identifier.check(componentArg)
? componentArg as namedTypes.Identifier
: undefined;
if (identifier) {
// Remove component identifiers like $1, $2 that may be appended
// and transform to kebab-case
return decamelize(identifier.name.split('$')[0], { separator: '-' });
}
return 'div'; // Default to div if we can't determine component
}
/**
* Extract props from the props argument
* @param {Object} propsArg - The props argument from _jsxDEV
* @return {Object} The extracted props
*/
function extractProps(propsArg: Node): Record<string, any> {
const objectExpression = propsArg && namedTypes.ObjectExpression.check(propsArg)
? propsArg as namedTypes.ObjectExpression
: undefined
if (!objectExpression) {
return {};
}
const result: Record<string, string | number | boolean | RegExp | null> = {};
objectExpression.properties.forEach((prop) => {
const property = namedTypes.Property.check(prop)
? prop as namedTypes.Property
: undefined;
if (!property || !namedTypes.Identifier.check(property.key)) {
return
}
const key = property.key.name;
// Extract the value based on the type
if (property.value.type === 'Literal') {
result[key] = property.value.value;
} else if (property.value.type === 'BooleanLiteral') {
result[key] = property.value.value;
} else if (property.value.type === 'ObjectExpression') {
result[key] = '{}'; // Placeholder for object expressions
} else if (property.value.type === 'ArrayExpression') {
result[key] = '[]'; // Placeholder for array expressions
}
});
return result;
}
/**
* Format props into HTML attributes
* @param {Object} props - The props object
* @return {string} Formatted HTML attributes
*/
function formatProps(props: Record<string, any>) {
const attributeStrings = [];
// Skip children since we'll handle it separately
for (const [key, value] of Object.entries(props)) {
if (key === 'children' || key === 'suppressHydrationWarning') continue;
// Handle boolean attributes
if (typeof value === 'boolean') {
if (value) {
attributeStrings.push(key);
}
continue;
}
// Handle string attributes
attributeStrings.push(`${key}="${escapeHtml(String(value))}"`);
}
return attributeStrings.join(' ');
}
/**
* Escape HTML special characters
* @param {string} str - String to escape
* @return {string} Escaped string
*/
function escapeHtml(str: string) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment