@zenithcore/core
Version:
Core functionality for ZenithKernel framework
957 lines (840 loc) • 32 kB
text/typescript
/* ZenithKernel Enhanced HTML Transformer
*
* Extends Archipelago's HtmlTransformer with ZenithKernel-specific features:
* - ZK proof verification and placeholders
* - ECS data binding and component integration
* - Hydration configuration and data attribute application
* - Enhanced context evaluation with ZK and ECS helpers
* - Graceful fallbacks for security and data operations
*/
import type { HydraContext } from './types';
import type { ECSManager, Entity } from '../../core/ECSManager';
import { ZenithParsedTemplate, ZenithTemplateParser } from './template-parser';
/**
* Enhanced render context with ZK and ECS integration
*/
export interface ZenithRenderContext {
// Base context from Archipelago
[key: string]: any;
// ZenithKernel-specific context
zkContext?: HydraContext;
ecsManager?: ECSManager;
verifySystem?: {
verifyProof: (entityId: string, zkProof: string) => Promise<boolean>;
isVerified: (entityId: string) => boolean;
};
// Helper functions available in template expressions
zkVerify?: (proof: string, entityId?: string) => Promise<boolean>;
ecsGet?: (entityId: Entity, componentType: string) => any;
ecsHas?: (entityId: Entity, componentType: string) => boolean;
ecsQuery?: (queryId: string) => Entity[];
}
/**
* Enhanced render options for ZenithKernel
*/
export interface ZenithRenderOptions {
// Base options from Archipelago
hydrate?: boolean;
ssr?: boolean;
clientOnly?: boolean;
// ZenithKernel-specific options
enableZKVerification?: boolean;
enableECSBinding?: boolean;
enableHydrationConfig?: boolean;
zkVerificationTimeout?: number;
fallbackToPlaceholder?: boolean;
debugMode?: boolean;
}
/**
* ZK verification result
*/
interface ZKVerificationResult {
isValid: boolean;
error?: string;
cached?: boolean;
}
/**
* Enhanced HTML Transformer for ZenithKernel
* Extends Archipelago's functionality with ZK verification and ECS integration
*/
export class ZenithHtmlTransformer {
private context: ZenithRenderContext;
private options: ZenithRenderOptions;
private zkVerificationCache = new Map<string, ZKVerificationResult>();
constructor(context: ZenithRenderContext, options: ZenithRenderOptions = {}) {
// Store context as reference to allow dynamic updates from tests
this.context = context;
// Clear cache on new instance to prevent test interference
this.zkVerificationCache.clear();
// Inject helper functions into context
this.context.zkVerify = this.createZKVerifyHelper();
this.context.ecsGet = this.createECSGetHelper();
this.context.ecsHas = this.createECSHasHelper();
this.context.ecsQuery = this.createECSQueryHelper();
this.options = {
enableZKVerification: true,
enableECSBinding: true,
enableHydrationConfig: true,
zkVerificationTimeout: 5000,
fallbackToPlaceholder: true,
debugMode: false,
...options
};
}
/**
* Transform a ZenithParsedTemplate into HTML
*/
public async transform(template: ZenithParsedTemplate): Promise<string> {
try {
// Check if template should be skipped (v-if evaluation)
if (await this.shouldSkip(template)) {
return '';
}
// Handle v-for loops
if (template.directives?.vFor) {
return await this.renderLoop(template);
}
// ZK verification check
if (this.options.enableZKVerification && template.zkDirectives?.zkProof) {
const zkResult = await this.verifyZKProof(template);
if (!zkResult.isValid) {
return this.renderZKVerificationPlaceholder(template, zkResult.error);
}
}
// ECS data binding
if (this.options.enableECSBinding && template.ecsBindings) {
await this.bindECSData(template);
}
// Build the HTML element
const tag = template.componentName || 'div';
const attrs = await this.buildAttributes(template);
const innerHTML = await this.buildInnerHTML(template);
// Apply hydration configuration
if (this.options.enableHydrationConfig && template.hydrationConfig) {
this.applyHydrationConfig(attrs, template.hydrationConfig);
}
// Client-only short circuit
if (this.options.clientOnly && !this.options.ssr) {
return `<${tag}${this.buildAttributeString(attrs)} />`;
}
// Interpolate any expressions in innerHTML
const interpolatedContent = await this.interpolate(innerHTML);
return `<${tag}${this.buildAttributeString(attrs)}>${interpolatedContent}</${tag}>`;
} catch (error) {
if (!this.options.fallbackToPlaceholder) {
throw error;
}
// Create error placeholder with debug info
const componentName = template.componentName || 'div';
const errorMsg = error instanceof Error ? error.message : 'Rendering error';
const attrs = { ...template.attributes };
// Add error classes and data
attrs['class'] = ['render-error-placeholder', attrs['class']].filter(Boolean).join(' ');
attrs['data-error'] = this.escapeHtml(errorMsg);
const attrString = this.buildAttributeString(attrs);
return `<${componentName}${attrString}>
<div class="render-error-placeholder">
<div class="error-message">
⚠️ Rendering Error
<small>${this.escapeHtml(errorMsg)}</small>
${this.options.debugMode ? `<pre class="error-details">${this.escapeHtml((error instanceof Error ? error.stack : '') || '')}</pre>` : ''}
</div>
</div>
</${componentName}>`;
}
}
/**
* Check if ZK proof verification is required and valid (v1.5 stub)
* Note: Full ZK verification will be implemented in v1.5
*/
private async verifyZKProof(template: ZenithParsedTemplate): Promise<ZKVerificationResult> {
const zkDirectives = template.zkDirectives;
if (!zkDirectives?.zkProof) {
return { isValid: true };
}
const proof = zkDirectives.zkProof;
const entityId = zkDirectives.zkEntity || this.context.zkContext?.peerId || 'test-peer';
// Check cache first
const cacheKey = `${entityId}:${proof}`;
if (this.zkVerificationCache.has(cacheKey)) {
return this.zkVerificationCache.get(cacheKey)!;
}
// v1.5 stub - simple verification behavior with timeout support
let result: ZKVerificationResult;
try {
// Call the verify system if available (for test compatibility)
if (this.context.verifySystem?.verifyProof) {
// Add timeout support
const timeout = this.options.zkVerificationTimeout || 5000;
const verificationPromise = this.context.verifySystem.verifyProof(entityId, proof);
const timeoutPromise = new Promise<boolean>((_, reject) =>
setTimeout(() => reject(new Error('ZK verification timeout')), timeout)
);
try {
const verificationResult = await Promise.race([verificationPromise, timeoutPromise]);
// Ensure we return a boolean
const isValid = Boolean(verificationResult);
result = { isValid };
} catch (timeoutError) {
if (timeoutError instanceof Error && timeoutError.message.includes('timeout')) {
result = { isValid: false, error: 'ZK verification timeout' };
} else {
throw timeoutError;
}
}
} else {
// v1.5 stub fallback - basic proof validation
// Fail for obviously invalid proofs, pass for others
if (proof.includes('invalid')) {
result = { isValid: false, error: 'ZK proof verification failed' };
} else {
result = { isValid: true };
}
}
} catch (error) {
result = {
isValid: false,
error: error instanceof Error ? error.message : 'ZK verification failed'
};
}
// Cache the result
this.zkVerificationCache.set(cacheKey, result);
return result;
}
/**
* Bind ECS data to the template context
*/
private async bindECSData(template: ZenithParsedTemplate): Promise<void> {
const ecsBindings = template.ecsBindings;
if (!ecsBindings || !this.context.ecsManager) {
return;
}
try {
// Bind entity data
if (ecsBindings.ecsEntity) {
const entityId = parseInt(ecsBindings.ecsEntity, 10);
if (!isNaN(entityId)) {
// Add entity ID to context
this.context.entityId = entityId;
// Add formatted entity ID string for display
this.context.entityIdStr = `Entity: ${entityId}`;
// Bind specific components if specified
if (ecsBindings.ecsComponents) {
for (const componentType of ecsBindings.ecsComponents) {
try {
// Get component data from ECS
const componentMap = this.context.ecsManager.dumpComponentMap().get(componentType);
const componentData = componentMap?.get(entityId);
if (componentData) {
// Add component data to context with safe key name
const safeKey = this.toSafeVariableName(componentType);
this.context[safeKey] = componentData;
// Add formatted display values
if (componentData.value !== undefined) {
this.context[`${safeKey}Display`] = `Value: ${componentData.value}`;
}
}
} catch (error) {
if (this.options.debugMode) {
console.warn(`Failed to bind ECS component ${componentType}:`, error);
}
}
}
}
// Auto-create entity if needed and specified
if (ecsBindings.ecsAutoCreate && !this.context.ecsManager.getAllEntities().includes(entityId)) {
// Note: In a real implementation, we'd need to handle entity creation more carefully
if (this.options.debugMode) {
console.info(`Entity ${entityId} would be auto-created`);
}
}
}
}
} catch (error) {
if (this.options.debugMode) {
console.error('ECS data binding failed:', error);
}
}
}
/**
* Apply hydration configuration as data attributes
*/
private applyHydrationConfig(attrs: Record<string, string>, config: any): void {
if (config.strategy) {
attrs['data-hydration-strategy'] = config.strategy;
}
if (config.priority) {
attrs['data-hydration-priority'] = config.priority;
}
if (config.lazy !== undefined) {
attrs['data-hydration-lazy'] = config.lazy.toString();
}
if (config.trigger) {
attrs['data-hydration-trigger'] = config.trigger;
}
if (config.debounce !== undefined) {
attrs['data-hydration-debounce'] = config.debounce.toString();
}
}
/**
* Build HTML attributes from template
*/
private async buildAttributes(template: ZenithParsedTemplate): Promise<Record<string, string>> {
const attrs: Record<string, string> = {};
// Process template bindings (:attr=\"expression\")
if (template.directives?.bindings) {
for (const [name, expr] of Object.entries(template.directives.bindings)) {
try {
const value = await this.evalInContext(expr);
attrs[name] = String(value);
} catch (error) {
attrs[name] = '';
if (this.options.debugMode) {
console.warn(`Failed to evaluate binding ${name}:`, error);
}
}
}
}
// Process static attributes (excluding directives)
for (const [name, value] of Object.entries(template.attributes)) {
if (!this.isDirectiveAttribute(name)) {
attrs[name] = await this.interpolate(value);
}
}
// Add hydration attributes if enabled
if (this.options.hydrate) {
attrs['data-hydrate'] = 'true';
if (template.componentName) {
attrs['data-component'] = template.componentName;
}
}
// Add ZK attributes
if (template.zkDirectives) {
const zkAttrs = ZenithTemplateParser.getZenithDataAttributes({ zkDirectives: template.zkDirectives } as any);
Object.assign(attrs, zkAttrs);
}
// Add ECS attributes
if (template.ecsBindings) {
const ecsAttrs = ZenithTemplateParser.getZenithDataAttributes({ ecsBindings: template.ecsBindings } as any);
Object.assign(attrs, ecsAttrs);
}
return attrs;
}
/**
* Build inner HTML content
*/
private async buildInnerHTML(template: ZenithParsedTemplate): Promise<string> {
try {
// Handle slots first
const slotContents = await Promise.all(
Object.entries(template.slots).map(async ([name, content]) => {
const interpolated = await this.interpolate(content);
return `<slot name="${this.escapeHtml(name)}">${interpolated}</slot>`;
})
);
// Handle direct content - use template.content directly since interpolate handles expressions
let content = '';
if (template.content) {
content = await this.interpolate(template.content);
} else if (template.attributes['content']) {
content = await this.interpolate(template.attributes['content']);
}
// Combine slots with content
const allContent = [
...slotContents,
content
].filter(Boolean);
return allContent.join('') || template.attributes['default-content'] || '';
} catch (error) {
// If fallback is disabled, re-throw the error
if (!this.options.fallbackToPlaceholder) {
throw error;
}
if (this.options.debugMode) {
console.warn('Failed to build inner HTML:', error);
return `[Error: Failed to build content]`;
}
return '';
}
}
/**
* Render a v-for loop
*/
private async renderLoop(template: ZenithParsedTemplate): Promise<string> {
const { item, iterable } = template.directives!.vFor!;
try {
const array = await this.evalInContext(iterable);
if (!Array.isArray(array)) {
return '';
}
const results = await Promise.all(
array.map(async (value, index) => {
const scopedContext = {
...this.context,
[item]: value,
index,
};
const instance = new ZenithHtmlTransformer(scopedContext, this.options);
const clone: ZenithParsedTemplate = {
...template,
directives: { ...template.directives, vFor: undefined }, // Prevent recursion
};
return await instance.transform(clone);
})
);
return results.join('');
} catch (error) {
if (this.options.debugMode) {
console.error('v-for rendering failed:', error);
}
return '';
}
}
/**
* Check if template should be skipped (v-if evaluation)
*/
private async shouldSkip(template: ZenithParsedTemplate): Promise<boolean> {
const expr = template.directives?.vIf;
if (!expr) return false;
try {
const result = await this.evalInContext(expr);
return !result;
} catch (error) {
if (this.options.debugMode) {
console.warn('v-if evaluation failed:', error);
}
return false;
}
}
/**
* Interpolate template expressions in content
*/
private async interpolate(content: string): Promise<string> {
if (!content) return '';
// Process all {{ expression }} patterns
const expressions = content.match(/\{\{\s*([^}]+?)\s*\}\}/g) || [];
let result = content;
for (const match of expressions) {
const expr = match.replace(/\{\{\s*|\s*\}\}/g, '');
try {
const value = await this.evalInContext(expr);
const stringValue = value != null ? String(value) : '';
result = result.replace(match, this.escapeHtml(stringValue));
} catch (error) {
if (this.options.debugMode) {
console.warn(`Interpolation failed for ${expr}:`, error);
}
// For missing variables, render empty (not error placeholder)
const errorMessage = error instanceof Error ? error.message : String(error);
// Only treat simple variable access as "missing variable", not function calls
if ((errorMessage.includes('is not defined') || errorMessage.includes('Cannot read property')) &&
!expr.includes('(') && !expr.includes(')')) {
result = result.replace(match, '');
} else {
// For function call errors and other serious errors, throw if fallback is disabled
if (!this.options.fallbackToPlaceholder) {
throw error;
}
// Otherwise render error placeholder
const errorMsg = error instanceof Error ? error.message : 'Rendering error';
const errorPlaceholder = `<span class="render-error-placeholder" data-error="${this.escapeHtml(errorMsg)}">⚠️ Rendering Error${this.options.debugMode ? ` <small>${this.escapeHtml(errorMsg)}</small>` : ''}</span>`;
result = result.replace(match, errorPlaceholder);
}
}
}
return result;
}
/**
* Evaluate an expression in the current context
*/
public async evalInContext(expr: string): Promise<any> {
try {
// Debug logging
if (this.options.debugMode) {
console.log('Evaluating expression:', expr);
console.log('Context keys:', Object.keys(this.context));
}
// Handle async expressions
const isAsync = expr.startsWith('await ');
if (isAsync) {
expr = expr.substring(6); // Remove 'await ' prefix
}
// Handle helper function calls directly
if (expr.includes('zkVerify(')) {
const match = expr.match(/zkVerify\((.+)\)/);
if (match) {
const args = match[1];
// Simple argument parsing for string literals
const argMatch = args.match(/^"([^"]+)"(?:,\s*"([^"]*)")?$/);
if (argMatch) {
const [, proof, entityId] = argMatch;
if (this.context.zkVerify) {
const result = await this.context.zkVerify(proof, entityId);
return result;
}
}
}
return false; // Default if zkVerify not available
}
if (expr.includes('ecsGet(')) {
const match = expr.match(/ecsGet\(([^,]+),\s*"([^"]+)"\)\.(\w+)/);
if (match) {
const [, entityIdStr, componentType, property] = match;
const entityId = parseInt(entityIdStr, 10);
if (this.context.ecsGet) {
const component = this.context.ecsGet(entityId, componentType);
return component?.[property];
}
}
return null; // Default if ecsGet not available
}
if (expr.includes('ecsHas(')) {
const match = expr.match(/ecsHas\(([^,]+),\s*"([^"]+)"\)/);
if (match) {
const [, entityIdStr, componentType] = match;
const entityId = parseInt(entityIdStr, 10);
if (this.context.ecsHas) {
return this.context.ecsHas(entityId, componentType);
}
}
return false; // Default if ecsHas not available
}
if (expr.includes('ecsQuery(')) {
const match = expr.match(/ecsQuery\("([^"]+)"\)\.(\w+)/);
if (match) {
const [, queryId, property] = match;
if (this.context.ecsQuery) {
const result = this.context.ecsQuery(queryId);
return result?.[property as keyof typeof result];
}
}
return null; // Default if ecsQuery not available
}
// Handle simple variable access first
if (expr in this.context && this.context[expr] !== undefined) {
return this.context[expr];
}
// Handle function calls that are not helper functions
if (expr.includes('(') && expr.includes(')')) {
const funcMatch = expr.match(/^(\w+)\(/);
if (funcMatch) {
const funcName = funcMatch[1];
// If it's not a known helper function and not in context, throw error
if (!['zkVerify', 'ecsGet', 'ecsHas', 'ecsQuery'].includes(funcName) &&
!(funcName in this.context)) {
throw new Error(`Function '${funcName}' is not defined`);
}
}
}
// Handle logical operators (&&, ||) - check BEFORE property access
if (expr.includes('&&') && !expr.includes('||')) {
const parts = expr.split('&&').map(p => p.trim());
// Evaluate each part and return true only if all are truthy
for (const part of parts) {
// Recursively evaluate each part using the same evalInContext logic
const partValue = await this.evalInContext(part);
// If any part is falsy, return false
if (!partValue) {
return false;
}
}
// All parts are truthy
return true;
}
if (expr.includes('||') && !expr.includes('&&')) {
const parts = expr.split('||').map(p => p.trim());
for (const part of parts) {
// Recursively evaluate each part using the same evalInContext logic
const partValue = await this.evalInContext(part);
// If any part is truthy, return true
if (partValue) {
return true;
}
}
return false;
}
// Handle ternary expressions (e.g., condition ? 'value1' : 'value2') - check BEFORE property access
if (expr.includes('?') && expr.includes(':')) {
// Find the ? and : positions, being careful about nested expressions
const questionIndex = expr.indexOf('?');
const colonIndex = expr.lastIndexOf(':');
if (questionIndex > 0 && colonIndex > questionIndex) {
const condition = expr.substring(0, questionIndex).trim();
const trueValue = expr.substring(questionIndex + 1, colonIndex).trim();
const falseValue = expr.substring(colonIndex + 1).trim();
const conditionResult = await this.evalInContext(condition);
const valueToEval = conditionResult ? trueValue : falseValue;
// Handle string literals
if ((valueToEval.startsWith("'") && valueToEval.endsWith("'")) ||
(valueToEval.startsWith('"') && valueToEval.endsWith('"'))) {
return valueToEval.slice(1, -1);
}
return await this.evalInContext(valueToEval);
}
}
// Handle property access (e.g., user.isAdmin) - check AFTER ternary expressions
if (expr.includes('.') && !expr.includes('&&') && !expr.includes('||') && !expr.includes('?') && !expr.includes(':')) {
const parts = expr.split('.');
let current: any = this.context;
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = current[part];
} else {
return undefined;
}
}
return current;
}
// Fallback to Function evaluation for complex expressions
const contextKeys = Object.keys(this.context);
const contextValues = Object.values(this.context);
try {
const funcBody = isAsync
? `
return (async () => {
try {
const result = await (${expr});
return result;
} catch (error) {
throw new Error('Expression evaluation failed: ' + error.message);
}
})();
`
: `
try {
const result = (${expr});
if (result instanceof Promise) {
return result;
}
return result;
} catch (error) {
throw new Error('Expression evaluation failed: ' + error.message);
}
`;
const func = new Function(...contextKeys, funcBody);
const result = await func(...contextValues);
return result;
} catch (funcError) {
// If Function evaluation fails, try a simpler approach for logical expressions
if (expr.includes('&&') || expr.includes('||')) {
// Simple logical evaluation without Function constructor
if (expr.includes('&&')) {
const parts = expr.split('&&').map(p => p.trim());
for (const part of parts) {
const result = await this.evalInContext(part);
if (!result) return false;
}
return true;
}
if (expr.includes('||')) {
const parts = expr.split('||').map(p => p.trim());
for (const part of parts) {
const result = await this.evalInContext(part);
if (result) return true;
}
return false;
}
}
throw funcError;
}
} catch (error) {
throw new Error(`Failed to evaluate expression "${expr}": ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Create ZK verification helper function (v1.5 stub)
* Note: Full ZK verification will be implemented in v1.5
*/
private createZKVerifyHelper() {
return async (proof: string, entityId?: string): Promise<boolean> => {
try {
if (this.context.verifySystem?.verifyProof) {
const id = entityId || this.context.zkContext?.peerId || 'test-peer';
return await this.context.verifySystem.verifyProof(id, proof);
}
// v1.5 stub - always return true for now
// TODO: Implement full ZK verification in v1.5
return true;
} catch (error) {
return false;
}
};
}
/**
* Create ECS get helper function
*/
private createECSGetHelper() {
return (entityId: Entity, componentType: string): any => {
try {
if (!this.context.ecsManager) return null;
// Use dumpComponentMap to get the component data
const allComponents = this.context.ecsManager.dumpComponentMap();
const componentMap = allComponents.get(componentType);
if (componentMap) {
return componentMap.get(entityId) || null;
}
return null;
} catch (error) {
if (this.options.debugMode) {
console.warn('ECS get failed:', error);
}
return null;
}
};
}
/**
* Create ECS has helper function
*/
private createECSHasHelper() {
return (entityId: Entity, componentType: string): boolean => {
try {
if (!this.context.ecsManager) return false;
// Use dumpComponentMap to check if component exists
const allComponents = this.context.ecsManager.dumpComponentMap();
const componentMap = allComponents.get(componentType);
return componentMap ? componentMap.has(entityId) : false;
} catch (error) {
if (this.options.debugMode) {
console.warn('ECS has check failed:', error);
}
return false;
}
};
}
/**
* Create ECS query helper function
*/
private createECSQueryHelper() {
return (queryId: string): Entity[] => {
try {
if (!this.context.ecsManager) return [];
return this.context.ecsManager.getEntitiesWithQuery(queryId);
} catch (error) {
if (this.options.debugMode) {
console.warn('ECS query failed:', error);
}
return [];
}
};
}
/**
* Render ZK verification placeholder
*/
private renderZKVerificationPlaceholder(template: ZenithParsedTemplate, error?: string): string {
const componentName = template.componentName || 'div';
const errorMsg = error || 'ZK proof verification failed';
const attrs = { ...template.attributes };
// Add ZK data attributes
if (template.zkDirectives) {
if (template.zkDirectives.zkProof) {
attrs['data-zk-proof'] = template.zkDirectives.zkProof;
}
if (template.zkDirectives.zkTrust) {
attrs['data-zk-trust'] = template.zkDirectives.zkTrust;
}
if (template.zkDirectives.zkEntity) {
attrs['data-zk-entity'] = template.zkDirectives.zkEntity;
}
if (template.zkDirectives.zkStrategy) {
attrs['data-zk-strategy'] = template.zkDirectives.zkStrategy;
}
}
// Add placeholder classes and error data
attrs['class'] = ['zk-verification-placeholder', attrs['class']].filter(Boolean).join(' ');
attrs['data-zk-error'] = errorMsg;
const attrString = this.buildAttributeString(attrs);
return `<${componentName}${attrString}>
<div class="zk-verification-placeholder">
<div class="zk-verification-message">
🔒 Verification Required
${this.options.debugMode ? `<small>${this.escapeHtml(errorMsg)}</small>` : ''}
</div>
</div>
</${componentName}>`;
}
/**
* Render error placeholder
*/
private renderErrorPlaceholder(template: ZenithParsedTemplate, error: any): string {
const componentName = template.componentName || 'div';
const errorMsg = error instanceof Error ? error.message : 'Rendering error';
const attrs = { ...template.attributes };
// Add error classes and data
attrs['class'] = ['render-error-placeholder', attrs['class']].filter(Boolean).join(' ');
attrs['data-error'] = this.escapeHtml(errorMsg);
const attrString = this.buildAttributeString(attrs);
return `<${componentName}${attrString}>
<div class="render-error-placeholder">
<div class="error-message">
⚠️ Rendering Error
${this.options.debugMode ? `<small>${this.escapeHtml(errorMsg)}</small>` : ''}
</div>
</div>
</${componentName}>`;
}
/**
* Check if an attribute is a directive
*/
private isDirectiveAttribute(name: string): boolean {
return name.startsWith('v-') ||
name.startsWith(':') ||
name.startsWith('zk-') ||
name.startsWith('data-zk-') ||
name.startsWith('ecs-') ||
name.startsWith('data-ecs-') ||
name.startsWith('hydration-') ||
name.startsWith('data-hydration-');
}
/**
* Build attribute string for HTML
*/
private buildAttributeString(attrs: Record<string, string>): string {
const attrString = Object.entries(attrs)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=\"${this.escapeHtml(String(value))}\"`)
.join(' ');
return attrString ? ' ' + attrString : '';
}
/**
* Convert component type name to safe variable name
*/
private toSafeVariableName(name: string): string {
return name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_$&');
}
/**
* Escape HTML characters
*/
private escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\"/g, '"')
.replace(/'/g, ''');
}
/**
* Create a transformer with minimal configuration for testing
*/
public static createMinimal(context: ZenithRenderContext = {}): ZenithHtmlTransformer {
return new ZenithHtmlTransformer(context, {
enableZKVerification: false,
enableECSBinding: false,
enableHydrationConfig: true,
fallbackToPlaceholder: true,
debugMode: true
});
}
/**
* Create a transformer with full production configuration
*/
public static createProduction(context: ZenithRenderContext): ZenithHtmlTransformer {
return new ZenithHtmlTransformer(context, {
enableZKVerification: true,
enableECSBinding: true,
enableHydrationConfig: true,
zkVerificationTimeout: 3000,
fallbackToPlaceholder: false,
debugMode: false
});
}
}