Created
March 20, 2025 13:07
-
-
Save christian-bromann/545df090961c57adcc71f3b2b9e6e8da to your computer and use it in GitHub Desktop.
This file contains 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
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, '&') | |
.replace(/</g, '<') | |
.replace(/>/g, '>') | |
.replace(/"/g, '"') | |
.replace(/'/g, '''); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment