Files
rspade_system/node_modules/@jqhtml/parser/dist/codegen.js
2025-12-26 21:49:28 +00:00

1406 lines
69 KiB
JavaScript

// JQHTML Code Generator - Converts AST to JavaScript functions
// Generates v1-compatible instruction arrays for efficient DOM construction
import { NodeType } from './ast.js';
import { SourceMapGenerator } from 'source-map';
import { JQHTMLParseError } from './errors.js';
import * as vm from 'vm';
export class CodeGenerator {
indent_level = 0;
components = new Map();
current_component = null;
in_slot = false;
tag_depth = 0;
lastOutput = ''; // Track last generated output for deduplication
// Position tracking for source maps
outputLine = 1;
outputColumn = 0;
sourceMapGenerator;
sourceContent;
sourceFile;
outputBuffer = [];
// Enable position tracking for debugging
enablePositionTracking = false;
positionLog = [];
// Line preservation for 1:1 source mapping
currentOutputLine = 1;
preserveLines = false;
// For true 1:1 line mapping
outputLines = [];
sourceLines = [];
generate(ast, sourceFile, sourceContent) {
// Reset state
this.components.clear();
this.current_component = null;
this.tag_depth = 0;
this.resetPositionTracking();
// Store source info for error reporting
this.sourceFile = sourceFile;
this.sourceContent = sourceContent;
// Process all top-level nodes
for (const node of ast.body) {
if (node.type === NodeType.COMPONENT_DEFINITION) {
this.generate_component(node);
}
}
// Build the final code
const code = this.build_module_code();
return {
code,
components: this.components
};
}
/**
* Generate code with source maps using 1:1 line mapping + SourceMapGenerator
*/
generateWithSourceMap(ast, sourceFile, sourceContent) {
// Use the regular generate method which creates 1:1 line mapping
const result = this.generate(ast, sourceFile, sourceContent);
// Now create a proper sourcemap for each component using SourceMapGenerator
const componentsWithSourcemaps = new Map();
for (const [name, component] of result.components) {
// Create a SourceMapGenerator for this component
const generator = new SourceMapGenerator({
file: sourceFile.replace(/\.jqhtml$/, '.js')
});
generator.setSourceContent(sourceFile, sourceContent);
// Parse the render function to find line count
const renderLines = component.render_function.split('\n');
// The first line is the function declaration - maps to the Define line (line 1)
generator.addMapping({
generated: { line: 1, column: 0 },
source: sourceFile,
original: { line: 1, column: 0 }
});
// Map ALL lines in the generated output
// This ensures that even lines beyond the source file length are mapped
const sourceLines = sourceContent.split('\n');
const numSourceLines = sourceLines.length;
// CRITICAL FIX: The render function has a consistent offset from source
// We need to map with the proper offset, not naive 1:1
// The parser generates code with a fixed offset (typically around 10-13 lines)
// Calculate the actual offset by looking at the generated code
// The render function's first real content line starts around line 2
// but it maps to source line 1 (the Define line)
// After that, we need to maintain proper offset mapping
// For now, use the simple 1:1 mapping with clamping
// The CLI's adjustSourcemapForCLI will handle the offset adjustment
for (let lineNum = 2; lineNum <= renderLines.length; lineNum++) {
// Map each line to its corresponding source line
// When we go beyond source length, clamp to last source line
const sourceLine = Math.min(lineNum, numSourceLines);
generator.addMapping({
generated: { line: lineNum, column: 0 },
source: sourceFile,
original: { line: sourceLine, column: 0 }
});
}
// Generate the sourcemap
const sourcemap = generator.toJSON();
const sourcemapJson = JSON.stringify(sourcemap);
const sourcemapBase64 = Buffer.from(sourcemapJson).toString('base64');
const sourceMapDataUri = `data:application/json;charset=utf-8;base64,${sourcemapBase64}`;
// Add inline sourcemap to render function
const renderWithSourcemap = component.render_function +
'\n//# sourceMappingURL=' + sourceMapDataUri;
componentsWithSourcemaps.set(name, {
...component,
render_function: renderWithSourcemap
});
}
return {
code: result.code,
components: componentsWithSourcemaps,
source_map: JSON.stringify({}), // Not used for inline sourcemaps
source_map_data_uri: '' // Not used for inline sourcemaps
};
}
generate_component_with_mappings_TESTING(node) {
// Extract metadata
let tagName = 'div';
const defaultAttributes = {};
const dependencies = new Set();
// Extract new fields from Define tag
const defineArgs = node.defineArgs;
const extendsValue = node.extends;
for (const [key, value] of Object.entries(node.attributes)) {
if (key === 'tag') {
if (typeof value === 'object' && value.quoted) {
tagName = value.value;
}
else if (typeof value === 'string') {
tagName = value;
}
}
else {
if (typeof value === 'object' && value.quoted) {
defaultAttributes[key] = value.value;
}
else if (typeof value === 'object' && value.expression) {
defaultAttributes[key] = value.value;
}
else if (typeof value === 'object' && value.identifier) {
defaultAttributes[key] = value.value;
}
else {
defaultAttributes[key] = value;
}
}
}
// Reset output tracking for this component
this.outputLine = 1;
this.outputColumn = 0;
this.outputBuffer = [];
// Generate function header
const header = 'function render(data, args, content, jqhtml) { let _output = []; const _cid = this._cid; const that = this;\n';
this.outputBuffer.push(header);
this.outputLine = 2; // We're now on line 2 after the header
// Generate function body - each node's emit() will track its position
for (const childNode of node.body) {
const childCode = this.generate_node(childNode);
if (childCode) {
// Don't add extra space - let the code maintain its position
this.outputBuffer.push(childCode);
}
}
// Add the return statement
this.outputBuffer.push('return [_output, this]; }');
// Combine all the output
const renderFunction = this.outputBuffer.join('');
// Store the component
this.components.set(node.name, {
name: node.name,
render_function: renderFunction,
dependencies: Array.from(dependencies),
tagName,
defaultAttributes,
defineArgs,
extends: extendsValue
});
}
generate_component(node) {
this.current_component = node.name;
this.lastOutput = ''; // Reset output tracking for each component
const dependencies = new Set();
// Always use 1:1 line mapping for proper sourcemaps
// Even when using SourceMapGenerator, we need the line structure
// Enable line preservation for 1:1 source maps
this.preserveLines = true;
this.currentOutputLine = 1;
// Initialize output lines array for 1:1 mapping
// We'll build the function line by line to match source structure
this.outputLines = [];
// Extract metadata and default attributes
// Handle the 'tag' attribute which might be a quoted object from the parser
let tagName = 'div';
const defaultAttributes = {};
// Extract new fields from Define tag (extends and defineArgs)
const defineArgs = node.defineArgs;
const extendsValue = node.extends;
// Process all attributes from the Define tag
for (const [key, value] of Object.entries(node.attributes)) {
if (key === 'tag') {
// Special handling for 'tag' attribute
if (typeof value === 'object' && value.quoted) {
tagName = value.value;
}
else if (typeof value === 'string') {
tagName = value;
}
}
else {
// Store as default attribute - extract actual value from quoted objects
if (typeof value === 'object' && value.quoted) {
defaultAttributes[key] = value.value;
}
else if (typeof value === 'object' && value.expression) {
// For expressions, we need to store them as is to be evaluated at runtime
defaultAttributes[key] = value.value;
}
else if (typeof value === 'object' && value.identifier) {
// For identifiers, store as is to be evaluated at runtime
defaultAttributes[key] = value.value;
}
else {
defaultAttributes[key] = value;
}
}
}
// Check if this is a slot-only template (template inheritance pattern)
if (node.isSlotOnly && node.slotNames && node.slotNames.length > 0) {
// Generate slot-only render function with 1:1 line mapping
// Extract slot nodes from body
const slots = {};
for (const child of node.body) {
if (child.type === NodeType.SLOT) {
const slotNode = child;
slots[slotNode.name] = slotNode;
}
}
// Filter out TEXT nodes (whitespace) from slot-only templates
// TEXT nodes cause _output.push() calls which are invalid in slot-only context
const slotsOnly = node.body.filter(child => child.type === NodeType.SLOT);
// Use 1:1 line mapping for slot-only templates
const bodyLines = this.generate_function_body_1to1(slotsOnly);
// Build the render function with line preservation
const lines = [];
// Line 1: function declaration returning slots object
lines.push(`function render(data, args, content, jqhtml) { return [{_slots: {`);
// Lines 2-N: Body lines with slot functions
lines.push(...bodyLines);
// Fix trailing comma on last slot function
// Find the last line that contains '.bind(this),' and remove the comma
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i] && lines[i].includes('.bind(this),')) {
lines[i] = lines[i].replace(/\.bind\(this\),([^,]*)$/, '.bind(this)$1');
break;
}
}
// Last line: close slots object and return
if (lines[lines.length - 1]) {
lines[lines.length - 1] += ' }}, this]; }';
}
else {
lines[lines.length - 1] = '}}, this]; }';
}
const render_function = lines.join('\n');
// Store component with slot-only metadata
this.components.set(node.name, {
name: node.name,
render_function,
dependencies: Array.from(dependencies),
tagName,
defaultAttributes,
defineArgs,
extends: extendsValue
});
this.current_component = null;
return;
}
// For 1:1 line mapping, we need to generate the body first to know how many lines it has
const bodyLines = this.generate_function_body_1to1(node.body);
// Now create the render function with 1:1 line mapping
// Line 1 of input (Define tag) becomes the function declaration
const lines = [];
// First line: function declaration and initial setup (corresponds to Define line)
lines.push(`function render(data, args, content, jqhtml) { let _output = []; const _cid = this._cid; const that = this;`);
// Body lines: each corresponds to a source line
lines.push(...bodyLines);
// Last line: closing (corresponds to closing Define tag)
if (lines[lines.length - 1]) {
lines[lines.length - 1] += ' return [_output, this]; }';
}
else {
lines[lines.length - 1] = 'return [_output, this]; }';
}
const render_function = lines.join('\n');
// Validate the generated function syntax using vm.Script for better error reporting
try {
const generatedCode = `
let _output = [];
const _cid = this._cid;
const that = this;
${bodyLines.join('\n')}
`;
new vm.Script(generatedCode, {
filename: this.sourceFile || 'component.jqhtml'
});
}
catch (error) {
// Extract JavaScript error details
const errorDetail = error.message || String(error);
let sourceLine = 1; // Default to component definition line
let sourceColumn = 0;
// Extract line number from vm.Script stack trace
// Stack format: "filename.jqhtml:LINE\n..." or "evalmachine.<anonymous>:LINE\n..."
if (error.stack && typeof error.stack === 'string') {
// Extract line number from first line of stack (before first newline)
const firstLine = error.stack.split('\n')[0];
const lineMatch = firstLine.match(/:(\d+)/);
if (lineMatch) {
const generatedLine = parseInt(lineMatch[1], 10);
// Map generated line to source line
// The bodyLines array has 1:1 correspondence with source lines
// Generated code includes 3-line header, so subtract that offset
const headerOffset = 3; // "let _output = [];\n const _cid = this._cid;\n const that = this;"
const bodyLineIndex = generatedLine - headerOffset - 1; // Convert to 0-based
if (bodyLineIndex >= 0 && bodyLineIndex < bodyLines.length) {
// Map to actual source line (bodyLines starts at source line 2)
// Subtract 1 because syntax errors typically get detected on the line AFTER the problem
sourceLine = Math.max(1, bodyLineIndex + 2 - 1);
}
}
}
// Build error message emphasizing unbalanced brackets
// Keep the helpful context about what to check for
const errorMessage = `Unbalanced bracket error in component "${node.name}": ${errorDetail}\n\nCheck for unclosed brackets, missing quotes, or invalid JavaScript syntax.`;
// Create proper JQHTMLParseError with file/line info for Laravel Ignition integration
throw new JQHTMLParseError(errorMessage, sourceLine, sourceColumn, this.sourceContent, this.sourceFile);
}
this.components.set(node.name, {
name: node.name,
render_function,
dependencies: Array.from(dependencies),
tagName, // Store metadata
defaultAttributes, // Store default attributes
defineArgs, // Store $ attributes from Define tag
extends: extendsValue // Store extends value for template inheritance
});
this.current_component = null;
}
generate_function_body(nodes, preserveLines = false) {
// For simplicity and better sourcemaps, generate everything on minimal lines
// This matches the rspade team's recommendation for 1:1 line mapping
const statements = [];
for (const node of nodes) {
const code = this.generate_node(node);
if (code) {
statements.push(code);
}
}
// Join all statements on a single line to minimize output lines
// This makes sourcemap generation much simpler
return statements.join(' ');
}
/**
* Generate function body with true 1:1 line mapping
* Each line in the source produces exactly one line in the output
*/
generate_function_body_1to1(nodes) {
// Find the max line number in the template body
let maxLine = 0;
const findMaxLine = (node) => {
if (node.line && node.line > maxLine) {
maxLine = node.line;
}
// Check various node types for children
if (node.body)
node.body.forEach(findMaxLine);
if (node.children)
node.children.forEach(findMaxLine);
if (node.consequent)
node.consequent.forEach(findMaxLine);
if (node.alternate)
node.alternate.forEach(findMaxLine);
};
nodes.forEach(findMaxLine);
// Initialize lines array with empty strings for each source line
const lines = [];
for (let i = 2; i <= maxLine; i++) {
lines.push('');
}
// Process nodes and build output line by line
// We need to handle nodes differently based on their type
const processNodeForLine = (node) => {
if (!node.line)
return;
// VALIDATION: Check for content() in <%!= %> blocks - must run before line filtering
if (node.type === NodeType.EXPRESSION) {
const expr = node;
let trimmedCode = expr.code.trim();
if (trimmedCode.endsWith(';')) {
trimmedCode = trimmedCode.slice(0, -1).trim();
}
const isContentCall = trimmedCode === 'content()' ||
trimmedCode.match(/^content\s*\(/);
if (isContentCall && !expr.escaped) {
const error = new JQHTMLParseError(`Invalid syntax: content() cannot be used in <%!= %> blocks`, expr.line, expr.column || 0, this.sourceContent, this.sourceFile);
error.suggestion =
`\n\ncontent() is a passthrough function - it passes inner HTML directly to the component.\n` +
`The concept of "escaping" or "raw" output doesn't apply here.\n\n` +
`Use <%= content() %> instead:\n\n` +
` ✗ Wrong: <%!= content() %> ← invalid, makes no sense\n` +
` ✓ Correct: <%= content() %> ← always use this form\n\n` +
`The same applies to named slots:\n` +
` ✓ <%= content('header') %>`;
throw error;
}
}
const lineIndex = node.line - 2; // Adjust for array index (line 2 = index 0)
if (lineIndex < 0 || lineIndex >= lines.length)
return;
// Generate code based on node type
switch (node.type) {
case NodeType.HTML_TAG: {
this.lastOutput = ''; // Reset for non-text output
const tag = node;
// Check if this is a raw content tag (textarea, pre)
if (tag.preserveWhitespace && !tag.selfClosing && tag.children && tag.children.length > 0) {
// Validate: only TEXT, EXPRESSION, and CODE_BLOCK children allowed in raw content tags
// HTML tags and components are not allowed (they would break whitespace preservation)
for (const child of tag.children) {
if (child.type !== NodeType.TEXT &&
child.type !== NodeType.EXPRESSION &&
child.type !== NodeType.CODE_BLOCK) {
const error = new JQHTMLParseError(`Invalid content in <${tag.name}> tag`, tag.line, tag.column || 0, this.sourceContent, this.sourceFile);
error.suggestion =
`\n\nAll content within <textarea> and <pre> tags must be plain text or expressions.\n` +
`HTML tags and components are not allowed.\n\n` +
`Allowed:\n` +
` <textarea><%= this.data.value %></textarea> ← expressions OK\n` +
` <textarea>plain text</textarea> ← plain text OK\n\n` +
`Not allowed:\n` +
` <textarea><div>content</div></textarea> ← HTML tags not OK\n` +
` <textarea><MyComponent /></textarea> ← components not OK\n\n` +
`This ensures proper whitespace preservation.`;
throw error;
}
}
// Generate rawtag instruction with raw content
const attrs_obj = this.generate_attributes_with_conditionals(tag.attributes, tag.conditionalAttributes);
// Collect raw content from children (all validated as TEXT)
let rawContent = '';
for (const child of tag.children) {
rawContent += child.content;
}
// Escape the raw content for JavaScript string
const escapedContent = this.escape_string(rawContent);
const rawtagInstruction = `_output.push({rawtag: ["${tag.name}", ${attrs_obj}, ${escapedContent}]});`;
lines[lineIndex] = (lines[lineIndex] || '') + rawtagInstruction;
}
else {
// Normal HTML tag processing
// Opening tag goes on its line
const openTag = this.generate_tag_open(tag);
if (openTag) {
lines[lineIndex] = (lines[lineIndex] || '') + openTag;
}
// Process children
if (tag.children) {
tag.children.forEach(processNodeForLine);
}
// Closing tag might be on a different line
const closeTag = `_output.push("</${tag.name}>");`;
// For simplicity, put closing tag on the last child's line or same line
const closeLine = tag.children && tag.children.length > 0
? (tag.children[tag.children.length - 1].line || node.line)
: node.line;
const closeIndex = closeLine - 2;
if (closeIndex >= 0 && closeIndex < lines.length) {
lines[closeIndex] = (lines[closeIndex] || '') + ' ' + closeTag;
}
}
break;
}
case NodeType.TEXT: {
const text = node;
// Apply padded trim to preserve intentional whitespace
const processed = this.padded_trim(text.content);
if (processed) {
const code = `_output.push(${this.escape_string(processed)});`;
// Optimization: skip consecutive identical space pushes
if (code === '_output.push(" ");' && this.lastOutput === '_output.push(" ");') {
// Skip duplicate - don't add to output
break;
}
this.lastOutput = code; // Track for next comparison
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
}
else {
// Empty text resets tracking so next space won't be skipped
this.lastOutput = '';
}
// Empty after processing: skip (no code generated)
break;
}
case NodeType.CODE_BLOCK: {
this.lastOutput = ''; // Reset for non-text output
const codeBlock = node;
const code = this.generate_code_block(codeBlock);
if (code) {
// With preprocessing, empty lines are already marked with /* empty line */
// So we can just add the code as-is, line by line
const codeLines = code.split('\n');
for (let i = 0; i < codeLines.length; i++) {
const targetIndex = lineIndex + i;
if (targetIndex < lines.length) {
// Add all lines including comment placeholders
const codeLine = codeLines[i].trim();
if (codeLine) {
lines[targetIndex] = (lines[targetIndex] || '') + ' ' + codeLines[i];
}
}
}
}
break;
}
case NodeType.EXPRESSION: {
this.lastOutput = ''; // Reset for non-text output
const expr = node;
// Generate the expression wrapper on a single line
let code;
// Special handling for content() calls
// Strip trailing semicolon if present (optional in <%= %> blocks)
// Note: content() validation happens at the top of processNodeForLine()
let trimmedCode = expr.code.trim();
if (trimmedCode.endsWith(';')) {
trimmedCode = trimmedCode.slice(0, -1).trim();
}
if (trimmedCode === 'content()') {
// Default slot/content - check _inner_html first
code = `(() => { if (this.args._inner_html) { _output.push(this.args._inner_html); } else if (typeof content === 'function') { const [contentInstructions] = content.call(this); _output.push(['_content', contentInstructions]); } })();`;
}
else if (trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/)) {
// Named slot: content('header') or content('header', data) (function call with string parameter and optional data)
// Use the standard result pattern for proper handling
code = `(() => { const result = ${trimmedCode};; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(jqhtml.escape_html(result)); } })();`;
}
else if (expr.escaped) {
code = `(() => { const result = ${expr.code}; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(jqhtml.escape_html(result)); } })();`;
}
else {
code = `(() => { const result = ${expr.code}; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(result); } })();`;
}
// Put the code on the starting line
if (code) {
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
}
break;
}
case NodeType.COMPONENT_INVOCATION: {
this.lastOutput = ''; // Reset for non-text output
const comp = node;
// For 1:1 mapping, generate compact component invocations
const attrs = this.generate_attributes_with_conditionals(comp.attributes, comp.conditionalAttributes);
if (comp.selfClosing || comp.children.length === 0) {
// Simple component without children
const code = `_output.push({comp: ["${comp.name}", ${attrs}]});`;
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
}
else {
// Check if children contain slots
const slots = this.extract_slots_from_children(comp.children);
if (Object.keys(slots).length > 0) {
// Component with slots - use generate_component_invocation for proper slot handling
const code = this.generate_component_invocation(comp);
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
}
else {
// Component with regular content (no slots)
// Generate inline content function
// Always include let _output = []; inside the function
lines[lineIndex] = (lines[lineIndex] || '') + ` _output.push({comp: ["${comp.name}", ${attrs}, function(${comp.name}) { let _output = [];`;
// Process children
if (comp.children && comp.children.length > 0) {
// Check if all children are simple text/inline nodes that can go on one line
const allInline = comp.children.every(child => child.type === NodeType.TEXT ||
(child.type === NodeType.COMPONENT_INVOCATION && child.selfClosing));
if (allInline) {
// Put all inline children on the same line as the component
comp.children.forEach(child => {
const childCode = this.generate_node(child);
if (childCode) {
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + childCode;
}
});
// Close on same line
lines[lineIndex] = (lines[lineIndex] || '') + ' return [_output, this]; }.bind(this)]});';
}
else {
// Multi-line children - process on their respective lines
comp.children.forEach(child => {
const origLine = child.line;
if (origLine && origLine >= 2) {
const childIndex = origLine - 2;
if (childIndex >= 0 && childIndex < lines.length) {
const childCode = this.generate_node(child);
if (childCode) {
lines[childIndex] = (lines[childIndex] || '') + ' ' + childCode;
}
}
}
});
// Return statement and closing on the last child's line
const returnLine = comp.children[comp.children.length - 1].line || node.line;
const returnIndex = returnLine - 2;
if (returnIndex >= 0 && returnIndex < lines.length) {
lines[returnIndex] = (lines[returnIndex] || '') + ' return [_output, this]; }.bind(this)]});';
}
}
}
else {
// No children - close on same line
lines[lineIndex] = (lines[lineIndex] || '') + ' return [_output, this]; }.bind(this)]});';
}
}
}
break;
}
case NodeType.SLOT: {
// Generate slot function definition with line mapping
const slot = node;
const slotName = slot.name;
// Start slot function on this line
lines[lineIndex] = (lines[lineIndex] || '') + ` ${slotName}: function(${slotName}) { const _output = [];`;
// Process slot children on their respective lines
if (slot.children && slot.children.length > 0) {
slot.children.forEach(child => {
const childLine = child.line;
if (childLine && childLine >= 2) {
const childIndex = childLine - 2;
if (childIndex >= 0 && childIndex < lines.length) {
const childCode = this.generate_node(child);
if (childCode) {
lines[childIndex] = (lines[childIndex] || '') + ' ' + childCode;
}
}
}
});
// Close slot function on the last child's line
const lastChild = slot.children[slot.children.length - 1];
const closeLine = lastChild.line || node.line;
const closeIndex = closeLine - 2;
if (closeIndex >= 0 && closeIndex < lines.length) {
lines[closeIndex] = (lines[closeIndex] || '') + ' return [_output, this]; }.bind(this),';
}
}
else {
// Empty slot - close on same line
lines[lineIndex] = (lines[lineIndex] || '') + ' return [_output, this]; }.bind(this),';
}
break;
}
default: {
// For other node types, use the standard generator
const code = this.generate_node(node);
if (code) {
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
}
}
}
};
// Process all top-level nodes
for (const node of nodes) {
processNodeForLine(node);
}
// For true 1:1 mapping, we must preserve ALL lines, even empty ones
// Each source line from 2 to maxLine should produce exactly one output line
// Don't remove any empty lines - they're essential for maintaining line positions
return lines;
}
generate_node(node) {
switch (node.type) {
case NodeType.TEXT:
return this.generate_text(node);
case NodeType.EXPRESSION:
return this.generate_expression(node);
case NodeType.HTML_TAG:
return this.generate_html_tag(node);
case NodeType.COMPONENT_INVOCATION:
return this.generate_component_invocation(node);
case NodeType.IF_STATEMENT:
return this.generate_if(node);
case NodeType.FOR_STATEMENT:
return this.generate_for(node);
case NodeType.CODE_BLOCK:
return this.generate_code_block(node);
default:
console.warn(`Unknown node type: ${node.type}`);
return '';
}
}
/**
* Padded trim: Collapse internal whitespace but preserve leading/trailing space
* Examples:
* " hello " → " hello "
* "hello" → "hello"
* " " → " "
* "\n\n \n" → " "
*/
padded_trim(text) {
const has_leading_space = /^\s/.test(text);
const has_trailing_space = /\s$/.test(text);
// Trim the text
let result = text.trim();
// Add back single space if original had leading/trailing whitespace
if (has_leading_space)
result = ' ' + result;
if (has_trailing_space)
result = result + ' ';
// Final pass: collapse all whitespace sequences to single space
return result.replace(/\s+/g, ' ');
}
generate_text(node) {
const content = node.content;
// Apply padded trim to preserve intentional whitespace
const processed = this.padded_trim(content);
// Skip if empty after processing
if (!processed) {
return '';
}
// Generate output code
const escaped = this.escape_string(processed);
const output = `_output.push(${escaped});`;
// Optimization: skip consecutive identical space pushes (but never skip newlines)
if (output === '_output.push(" ");' && this.lastOutput === '_output.push(" ");') {
return ''; // Skip duplicate space push
}
// Track this output for next comparison
this.lastOutput = output;
// Track the emitted position with source mapping
if (this.enablePositionTracking) {
this.emit(output, node);
}
return output;
}
generate_expression(node) {
this.lastOutput = ''; // Reset for non-text output
let output;
// Special handling for content() calls
// Strip trailing semicolon if present (optional in <%= %> blocks)
let trimmedCode = node.code.trim();
if (trimmedCode.endsWith(';')) {
trimmedCode = trimmedCode.slice(0, -1).trim();
}
// VALIDATION: content() must be used with <%= %>, not <%!= %>
// content() is a passthrough function - its output is already HTML, not a string that needs escaping
const isContentCall = trimmedCode === 'content()' ||
trimmedCode.match(/^content\s*\(/);
if (isContentCall && !node.escaped) {
const error = new JQHTMLParseError(`Invalid syntax: content() cannot be used in <%!= %> blocks`, node.line, node.column || 0, this.sourceContent, this.sourceFile);
error.suggestion =
`\n\ncontent() is a passthrough function - it passes inner HTML directly to the component.\n` +
`The concept of "escaping" or "raw" output doesn't apply here.\n\n` +
`Use <%= content() %> instead:\n\n` +
` ✗ Wrong: <%!= content() %> ← invalid, makes no sense\n` +
` ✓ Correct: <%= content() %> ← always use this form\n\n` +
`The same applies to named slots:\n` +
` ✓ <%= content('header') %>`;
throw error;
}
if (trimmedCode === 'content()') {
// Default slot/content - check _inner_html first
output = `(() => { if (this.args._inner_html) { _output.push(this.args._inner_html); } else if (typeof content === 'function') { const [contentInstructions] = content.call(this); _output.push(['_content', contentInstructions]); } })();`;
}
else if (trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/)) {
// Named slot: content('header') or content('header', data) (function call with string parameter and optional data)
// Use the standard result pattern for proper handling
output = `(() => { const result = ${trimmedCode};; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(jqhtml.escape_html(result)); } })();`;
}
else if (node.escaped) {
// Single-line expression handler for escaped output
output = `(() => { const result = ${node.code}; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(jqhtml.escape_html(result)); } })();`;
}
else {
// Single-line expression handler for unescaped output
output = `(() => { const result = ${node.code}; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(result); } })();`;
}
// Track the emitted position with source mapping
if (this.enablePositionTracking) {
this.emit(output, node);
}
return output;
}
generate_if(node) {
// Clean up condition - remove trailing opening brace
let condition = node.condition.trim();
condition = condition.replace(/\s*{\s*$/, ''); // Remove trailing brace
// Generate consequent body inline
const consequent_parts = [];
for (const child of node.consequent) {
const child_code = this.generate_node(child);
if (child_code) {
consequent_parts.push(child_code);
}
}
// Generate if statement - keep on single line for line preservation
let code = `if (${condition}) { ${consequent_parts.join(' ')} }`;
// Add else clause if present
if (node.alternate && node.alternate.length > 0) {
const alternate_parts = [];
for (const child of node.alternate) {
const child_code = this.generate_node(child);
if (child_code) {
alternate_parts.push(child_code);
}
}
code += ` else { ${alternate_parts.join(' ')} }`;
}
// Track the emitted position with source mapping
if (this.enablePositionTracking) {
this.emit(code, node);
}
return code;
}
generate_for(node) {
const iterator = node.iterator.trim();
// Generate body inline
const body_parts = [];
for (const child of node.body) {
const child_code = this.generate_node(child);
if (child_code) {
body_parts.push(child_code);
}
}
// Generate for loop - keep on single line for line preservation
const code = `for ${iterator} { ${body_parts.join(' ')} }`;
// Track the emitted position with source mapping
if (this.enablePositionTracking) {
this.emit(code, node);
}
return code;
}
generate_code_block(node) {
// Handle legacy code property for backward compatibility
if (node.code !== undefined) {
return node.code;
}
// Process tokens to transform PHP-style syntax
if (!node.tokens || node.tokens.length === 0) {
return '';
}
const result = [];
for (let i = 0; i < node.tokens.length; i++) {
const token = node.tokens[i];
const nextToken = node.tokens[i + 1];
switch (token.type) {
case 'IF':
result.push('if');
break;
case 'ELSE':
// Check if next token is IF for "else if"
if (nextToken && nextToken.type === 'IF') {
result.push('} else');
// Don't skip the IF token, it will be processed next
}
else if (nextToken && nextToken.type === 'JAVASCRIPT') {
// Check if this is "else if" in the JavaScript token
if (nextToken.value.trim().startsWith('if ')) {
result.push('} else');
// The 'if' is part of the JAVASCRIPT token, will be handled there
}
else if (nextToken.value.trim() === ':') {
// Regular else with colon in next token
result.push('} else {');
i++; // Skip the colon token
}
else {
// else with other JavaScript code
result.push('} else {');
}
}
else {
// else without anything after (shouldn't happen in valid JQHTML)
result.push('} else {');
}
break;
case 'ELSEIF':
result.push('} else if');
break;
case 'ENDIF':
result.push('}');
// Skip optional semicolon in next JAVASCRIPT token
if (nextToken && nextToken.type === 'JAVASCRIPT' && nextToken.value.trim() === ';') {
i++; // Skip the semicolon token
}
break;
case 'FOR':
result.push('for');
break;
case 'ENDFOR':
result.push('}');
// Skip optional semicolon in next JAVASCRIPT token
if (nextToken && nextToken.type === 'JAVASCRIPT' && nextToken.value.trim() === ';') {
i++; // Skip the semicolon token
}
break;
case 'JAVASCRIPT':
// Transform colons to opening braces after conditions
let jsCode = token.value;
// Check if this is a condition ending with colon
const prevToken = i > 0 ? node.tokens[i - 1] : null;
if (jsCode.endsWith(':') &&
prevToken &&
(prevToken.type === 'IF' || prevToken.type === 'FOR' || prevToken.type === 'ELSEIF' ||
(prevToken.type === 'ELSE' && jsCode.startsWith('if ')))) {
jsCode = jsCode.slice(0, -1) + ' {';
}
result.push(jsCode);
break;
default:
// Pass through any other token types
result.push(token.value);
}
}
const code = result.join(' ');
// Track the emitted position with source mapping
if (this.enablePositionTracking) {
this.emit(code, node);
}
return code;
}
generate_tag_open(node) {
// Generate just the opening tag
const attrs = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
return `_output.push({tag: ["${node.name}", ${attrs}, ${node.selfClosing || false}]});`;
}
generate_html_tag(node) {
this.lastOutput = ''; // Reset for non-text output
// Check if this tag needs raw content preservation
if (node.preserveWhitespace && !node.selfClosing && node.children.length > 0) {
// Validate: only TEXT, EXPRESSION, and CODE_BLOCK children allowed in raw content tags
// HTML tags and components are not allowed (they would break whitespace preservation)
for (const child of node.children) {
if (child.type !== NodeType.TEXT &&
child.type !== NodeType.EXPRESSION &&
child.type !== NodeType.CODE_BLOCK) {
const error = new JQHTMLParseError(`Invalid content in <${node.name}> tag`, node.line, node.column || 0, this.sourceContent, this.sourceFile);
error.suggestion =
`\n\nAll content within <textarea> and <pre> tags must be plain text or expressions.\n` +
`HTML tags and components are not allowed.\n\n` +
`Allowed:\n` +
` <textarea><%= this.data.value %></textarea> ← expressions OK\n` +
` <textarea>plain text</textarea> ← plain text OK\n\n` +
`Not allowed:\n` +
` <textarea><div>content</div></textarea> ← HTML tags not OK\n` +
` <textarea><MyComponent /></textarea> ← components not OK\n\n` +
`This ensures proper whitespace preservation.`;
throw error;
}
}
// Generate rawtag instruction with raw content
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
// Collect raw content from children (all validated as TEXT)
let rawContent = '';
for (const child of node.children) {
rawContent += child.content;
}
// Escape the raw content for JavaScript string
const escapedContent = this.escape_string(rawContent);
return `_output.push({rawtag: ["${node.name}", ${attrs_obj}, ${escapedContent}]});`;
}
// Normal tag generation
const parts = [];
// Generate opening tag instruction
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
parts.push(`_output.push({tag: ["${node.name}", ${attrs_obj}, ${node.selfClosing}]});`);
if (!node.selfClosing) {
// Generate children inline
if (node.children.length > 0) {
for (const child of node.children) {
const child_code = this.generate_node(child);
if (child_code) {
// Skip empty text nodes
if (!child_code.match(/^_output\.push\(""\);?$/)) {
parts.push(child_code);
}
}
}
}
// Generate closing tag
parts.push(`_output.push("</${node.name}>");`);
}
// Join parts with space for single-line tags, but this will be split later
return parts.join(' ');
}
generate_component_invocation(node) {
this.lastOutput = ''; // Reset for non-text output
const instructions = [];
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
if (node.selfClosing || node.children.length === 0) {
// Simple component without children
const componentCall = `_output.push({comp: ["${node.name}", ${attrs_obj}]});`;
instructions.push(componentCall);
// Track component invocation position
if (this.enablePositionTracking) {
this.emit(componentCall, node);
}
}
else {
// Check if children contain slots
const slots = this.extract_slots_from_children(node.children);
if (Object.keys(slots).length > 0) {
// Component with slots - generate content as object with slot functions ON A SINGLE LINE
const slotEntries = [];
for (const [slotName, slotNode] of Object.entries(slots)) {
const slot_code = slotNode.children.length > 0
? this.generate_function_body(slotNode.children)
: '';
// Add slot name as parameter: function(slotName) { ... }
slotEntries.push(`${slotName}: function(${slotName}) { const _output = []; ${slot_code} return [_output, this]; }.bind(this)`);
}
// Everything on one line
instructions.push(`_output.push({comp: ["${node.name}", ${attrs_obj}, {${slotEntries.join(', ')}}]});`);
}
else {
// Component with regular content (no slots)
instructions.push(`_output.push({comp: ["${node.name}", ${attrs_obj}, function(${node.name}) {`);
instructions.push(` const _output = [];`);
const children_code = this.generate_function_body(node.children);
if (children_code) {
instructions.push(this.indent(children_code, 1));
}
instructions.push(` return [_output, this];`);
instructions.push(`}.bind(this)]});`);
}
}
return instructions.join('\n');
}
parse_attributes(attrs_string) {
const attrs = {};
// Simple attribute parser - handles key="value" and key=value
const attr_regex = /([a-zA-Z$][a-zA-Z0-9-]*)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/g;
let match;
while ((match = attr_regex.exec(attrs_string)) !== null) {
const [, name, value] = match;
// Remove quotes if present
attrs[name] = value.replace(/^["']|["']$/g, '');
}
return attrs;
}
// Generate attribute object including conditional attributes
generate_attributes_with_conditionals(attrs, conditionalAttrs) {
// If no conditional attributes, use simple object
if (!conditionalAttrs || conditionalAttrs.length === 0) {
return this.generate_attributes_object(attrs);
}
// We have conditional attributes - need to merge them at runtime
const baseAttrs = this.generate_attributes_object(attrs);
// Generate code that conditionally adds attributes
let result = baseAttrs;
for (const condAttr of conditionalAttrs) {
const condAttrsObj = this.generate_attributes_object(condAttr.attributes);
// Use Object.assign to merge conditional attributes
result = `Object.assign({}, ${result}, (${condAttr.condition}) ? ${condAttrsObj} : {})`;
}
return result;
}
generate_attributes_object(attrs) {
if (Object.keys(attrs).length === 0) {
return '{}';
}
const entries = Object.entries(attrs).flatMap(([key, value]) => {
// Convert 'tag' to '_tag' for component invocations
const attrKey = key === 'tag' ? '_tag' : key;
// Special handling for data-sid attribute (from $sid) - create scoped id
// NOTE: Parser converts $sid="foo" → data-sid="foo" so we can distinguish from regular id
// This generates: id="foo:PARENT_CID" data-sid="foo"
// The :PARENT_CID scoping happens at runtime in instruction-processor.ts
if (key === 'data-sid') {
const id_entries = [];
if (value && typeof value === 'object' && value.interpolated) {
// Interpolated $sid like $sid="user<%= index %>"
const parts = value.parts.map((part) => {
if (part.type === 'text') {
return this.escape_string(part.value);
}
else {
return part.value;
}
});
const base_id = parts.join(' + ');
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-sid": ${base_id}`);
}
else if (value && typeof value === 'object' && value.quoted) {
// Quoted $sid like $sid="static"
const base_id = this.escape_string(value.value);
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-sid": ${base_id}`);
}
else {
// Simple $sid like $sid="username" or expression like $sid=someVar
const base_id = this.escape_string(String(value));
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-sid": ${base_id}`);
}
return id_entries;
}
// Regular id attribute - pass through unchanged
// id="foo" remains id="foo" (no scoping)
if (key === 'id') {
if (value && typeof value === 'object' && value.quoted) {
return `"id": ${this.escape_string(value.value)}`;
}
else {
return `"id": ${this.escape_string(String(value))}`;
}
}
// Check if this is an interpolated attribute value
if (value && typeof value === 'object' && value.interpolated) {
// Build concatenation expression
const parts = value.parts.map((part) => {
if (part.type === 'text') {
return this.escape_string(part.value);
}
else {
// Expression - wrap in parentheses to preserve operator precedence
// This ensures "a" + (x ? 'b' : 'c') instead of "a" + x ? 'b' : 'c'
return `(${part.value})`;
}
});
return `"${attrKey}": ${parts.join(' + ')}`;
}
// Check if value is marked as quoted
if (value && typeof value === 'object' && value.quoted) {
// This was a quoted string in the source - always treat as string
return `"${attrKey}": ${this.escape_string(value.value)}`;
}
// Check if it's a parenthesized expression: $attr=(expr)
if (value && typeof value === 'object' && value.expression) {
// It's an expression - output without quotes
return `"${attrKey}": ${value.value}`;
}
// Check if it's a bare identifier: $attr=identifier
if (value && typeof value === 'object' && value.identifier) {
// It's an identifier - output as JavaScript expression
return `"${attrKey}": ${value.value}`;
}
// Check if it's an event handler binding (data-__-on-*)
if (key.startsWith('data-__-on-')) {
// Handle based on whether value was quoted or not
if (typeof value === 'object' && value !== null) {
if (value.quoted) {
// Was quoted: @click="handler" -> string literal
return `"${key}": ${this.escape_string(value.value)}`;
}
else if (value.identifier || value.expression) {
// Was unquoted: @click=this.handler -> JavaScript expression
return `"${key}": ${value.value}`;
}
else if (value.interpolated) {
// Has interpolation - needs special handling
return `"${key}": ${this.compile_interpolated_value(value)}`;
}
}
else if (typeof value === 'string') {
// Fallback for simple strings (backwards compatibility)
return `"${key}": ${this.escape_string(value)}`;
}
}
// Check if it's a data attribute ($foo or :binding)
if (key.startsWith('data-')) {
// Handle based on whether value was quoted or not
if (typeof value === 'object' && value !== null) {
if (value.quoted) {
// Was quoted: $foo="bar" -> string literal
return `"${key}": ${this.escape_string(value.value)}`;
}
else if (value.identifier || value.expression) {
// Was unquoted: $foo=this.data.bar -> JavaScript expression
return `"${key}": ${value.value}`;
}
else if (value.interpolated) {
// Has interpolation - needs special handling
return `"${key}": ${this.compile_interpolated_value(value)}`;
}
}
else if (typeof value === 'string') {
// Fallback for simple strings
return `"${key}": ${this.escape_string(value)}`;
}
}
// Regular attributes
if (typeof value === 'object' && value !== null) {
if (value.quoted) {
// Explicitly quoted value
return `"${attrKey}": ${this.escape_string(value.value)}`;
}
else if (value.identifier || value.expression) {
// Unquoted JavaScript expression
return `"${attrKey}": ${value.value}`;
}
else if (value.interpolated) {
// Has interpolation
return `"${attrKey}": ${this.compile_interpolated_value(value)}`;
}
}
// Default: treat as string
return `"${attrKey}": ${this.escape_string(String(value))}`;
});
return `{${entries.join(', ')}}`;
}
is_self_closing_tag(tag_name) {
const self_closing = [
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'link', 'meta', 'param', 'source', 'track', 'wbr'
];
return self_closing.includes(tag_name.toLowerCase());
}
compile_interpolated_value(value) {
// Handle interpolated values with embedded expressions
if (!value.parts || !Array.isArray(value.parts)) {
return this.escape_string(String(value));
}
// Build template literal from parts
const parts = value.parts.map((part) => {
if (part.type === 'text') {
// Escape text parts for template literal
const escaped = part.value
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\${/g, '\\${');
return escaped;
}
else if (part.type === 'expression') {
// Embed expression
return `\${${part.value}}`;
}
return part.value;
});
// Return as template literal
return '`' + parts.join('') + '`';
}
escape_string(str) {
// Escape for JavaScript string literal
const escaped = str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
return `"${escaped}"`;
}
indent(code, level) {
const indent = ' '.repeat(level);
return code.split('\n').map(line => line ? indent + line : line).join('\n');
}
extract_slots_from_children(children) {
const slots = {};
for (const child of children) {
if (child.type === NodeType.SLOT) {
const slotNode = child;
slots[slotNode.name] = slotNode;
}
}
return slots;
}
// -------------------------------------------------------------------------
// Position Tracking Methods
// -------------------------------------------------------------------------
/**
* Emit text and track position for source maps
*/
emit(text, node) {
// Store the starting position before emitting
const startLine = this.outputLine;
const startColumn = this.outputColumn;
// Update position tracking
for (let i = 0; i < text.length; i++) {
if (text[i] === '\n') {
this.outputLine++;
this.outputColumn = 0;
}
else {
this.outputColumn++;
}
}
// Log position if tracking enabled
if (this.enablePositionTracking) {
this.positionLog.push({
line: this.outputLine,
column: this.outputColumn,
text: text.substring(0, 20), // First 20 chars for debugging
node: node ? `${node.type}:L${node.line}:C${node.column}` : undefined
});
}
// ARCHIVED: Token-by-token mapping approach - not currently used
// We're using 1:1 line mapping instead, but keeping this code for potential future use
/* istanbul ignore next */
if (false) {
// TypeScript can't analyze dead code properly, so we cast to any
const deadCode = () => {
if (this.sourceMapGenerator && node?.loc && this.sourceFile) {
this.sourceMapGenerator.addMapping({
generated: { line: startLine, column: startColumn },
source: this.sourceFile,
original: { line: node.loc.start.line, column: node.loc.start.column - 1 },
name: undefined
});
}
};
}
this.outputBuffer.push(text);
return text;
}
/**
* Emit a line of text (adds newline)
*/
emitLine(text, node) {
return this.emit(text + '\n', node);
}
/**
* Reset position tracking
*/
resetPositionTracking() {
this.outputLine = 1;
this.outputColumn = 0;
this.outputBuffer = [];
this.positionLog = [];
}
/**
* Get position tracking log for debugging
*/
getPositionLog() {
return this.positionLog;
}
/**
* Enable/disable position tracking
*/
setPositionTracking(enabled) {
this.enablePositionTracking = enabled;
}
/**
* Serialize attribute object with proper handling of identifiers and expressions
* Quoted values become strings, identifiers/expressions become raw JavaScript
*/
serializeAttributeObject(obj) {
if (!obj || Object.keys(obj).length === 0) {
return '{}';
}
const entries = [];
for (const [key, value] of Object.entries(obj)) {
// Check if value is a parsed attribute object with type info
if (value && typeof value === 'object' && (value.identifier || value.expression)) {
// Identifier or expression - output as raw JavaScript (no quotes)
entries.push(`"${key}": ${value.value}`);
}
else if (value && typeof value === 'object' && value.quoted) {
// Quoted string - output as string literal
entries.push(`"${key}": ${JSON.stringify(value.value)}`);
}
else {
// Simple value - output as-is via JSON.stringify
entries.push(`"${key}": ${JSON.stringify(value)}`);
}
}
return `{${entries.join(', ')}}`;
}
build_module_code() {
let code = '// Generated by JQHTML v2 Code Generator\n';
code += '// Parser version: 2.2.56 (with 1:1 sourcemap fixes)\n';
code += '// Produces v1-compatible instruction arrays\n\n';
// Add html escaping function (assumed to be available in runtime)
code += '// Runtime should provide: function html(str) { return escapeHtml(str); }\n\n';
// Add component registration
code += 'const jqhtml_components = new Map();\n\n';
// Add each component
for (const [name, component] of this.components) {
code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${name}', {\n`;
code += ` _jqhtml_version: '2.3.32',\n`; // Version will be replaced during build
code += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;
// Add defineArgs if present ($ attributes on Define tag)
if (component.defineArgs) {
code += ` defineArgs: ${this.serializeAttributeObject(component.defineArgs)},\n`;
}
// Add extends if present (template inheritance)
if (component.extends) {
code += ` extends: '${component.extends}',\n`;
}
code += ` render: ${this.indent(component.render_function, 1).trim()},\n`;
code += ` dependencies: [${component.dependencies.map(d => `'${d}'`).join(', ')}]\n`;
code += '});\n\n';
}
// Add export
code += 'export { jqhtml_components };\n';
return code;
}
}
// Helper function for standalone usage
export function generate(ast, sourceFile, sourceContent) {
const generator = new CodeGenerator();
return generator.generate(ast, sourceFile, sourceContent);
}
//# sourceMappingURL=codegen.js.map