Files
rspade_system/node_modules/@jqhtml/parser/dist/compiler.js
root 5295013f73 Update jqhtml packages
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 16:40:23 +00:00

246 lines
9.5 KiB
JavaScript

/**
* Unified JQHTML Compiler Module
*
* Single source of truth for compiling JQHTML templates to JavaScript
* with proper sourcemap generation and version injection.
*/
import { Lexer } from './lexer.js';
import { Parser } from './parser.js';
import { CodeGenerator } from './codegen.js';
/**
* Compile a JQHTML template to JavaScript
*
* @param source - The JQHTML template source code
* @param filename - The source filename for sourcemap generation
* @param options - Compilation options
* @returns The compiled JavaScript code as a string
*/
export function compileTemplate(source, filename, options) {
// Validate: content() must be called with <%= %> not <% %>
if (source.includes('<% content()')) {
throw new Error(`Invalid content() usage in ${filename}:\n\n` +
`content() must be output using <%= %> tags, not <% %> tags.\n\n` +
`Wrong: <% content() %>\n` +
`Right: <%= content() %>\n\n` +
`This ensures the content is properly rendered as output.`);
}
// 1. Parse the template
const lexer = new Lexer(source);
const tokens = lexer.tokenize();
const parser = new Parser(tokens, source, filename);
const ast = parser.parse();
// 2. Generate code WITHOUT sourcemap (we'll create it after wrapping)
const generator = new CodeGenerator();
const result = generator.generate(ast, filename, source);
// 3. Get component info
const componentName = result.components.keys().next().value;
if (!componentName) {
throw new Error('No component found in template');
}
const component = result.components.get(componentName);
if (!component) {
throw new Error(`Component ${componentName} not found in results`);
}
// 4. Apply format wrapping and version injection
const version = options.version || getPackageVersion();
let output = formatOutput(component, componentName, options.format, version);
// 5. Generate sourcemap AFTER wrapping (if requested)
if (options.sourcemap) {
const sourcemap = generateSourcemapForWrappedCode(output, source, filename);
output = output + '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' + sourcemap;
}
return {
code: output,
componentName
};
}
/**
* Get package version from package.json
* We can't use dynamic import in TypeScript, so we'll inject at build time
*/
function getPackageVersion() {
// This will be replaced by the build process
// The CLI will pass the version directly
return '__PARSER_VERSION__';
}
/**
* Serialize defineArgs/defaultAttributes with proper handling of identifiers and expressions
* Quoted values become strings, identifiers/expressions become raw JavaScript
*/
function 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(', ')}}`;
}
/**
* Format the generated code according to the specified module format
* Moved from CLI compiler's formatOutput function
*/
function formatOutput(componentInfo, componentName, format, version) {
const name = componentInfo.name;
// Build the component definition
let componentDef = `{
__jqhtml_template: true,
_jqhtml_version: '${version}',
name: '${componentInfo.name}',
tag: '${componentInfo.tagName}',
defaultAttributes: ${serializeAttributeObject(componentInfo.defaultAttributes)},`;
// Add defineArgs if present ($ attributes from Define tag)
if (componentInfo.defineArgs) {
componentDef += `\n defineArgs: ${serializeAttributeObject(componentInfo.defineArgs)},`;
}
// Add extends if present (template inheritance)
if (componentInfo.extends) {
componentDef += `\n extends: '${componentInfo.extends}',`;
}
componentDef += `\n render: ${componentInfo.render_function.trimEnd()},
dependencies: ${JSON.stringify(componentInfo.dependencies)}
}`;
let output;
switch (format) {
case 'iife':
// Variable assignment - no auto-registration
// Consumer must call jqhtml.register_template() explicitly
output = `// Compiled from: ${componentName}.jqhtml
var template_${name} = ${componentDef};`;
break;
case 'esm':
// ES Module export - no auto-registration
// Consumer must call jqhtml.register_template() explicitly
output = `// ES Module: ${name}
const template_${name} = ${componentDef};
export { template_${name} };
export default template_${name};`;
break;
case 'cjs':
// CommonJS export - no auto-registration
// Consumer must call jqhtml.register_template() explicitly
output = `// CommonJS Module: ${name}
'use strict';
const template_${name} = ${componentDef};
module.exports = template_${name};
module.exports.default = template_${name};
module.exports.template_${name} = template_${name};`;
break;
case 'umd':
// Universal Module Definition - no auto-registration
// Consumer must call jqhtml.register_template() explicitly
output = `(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory();
} else {
// Browser global
root.template_${name} = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
return ${componentDef};
}));`;
break;
default:
throw new Error(`Unknown format: ${format}`);
}
return output;
}
/**
* Generate a sourcemap for already-wrapped code
* This generates sourcemaps AFTER the wrapper has been applied
*/
function generateSourcemapForWrappedCode(wrappedCode, sourceContent, filename) {
// Count lines in wrapped output and source
const outputLines = wrappedCode.split('\n').length;
const sourceLines = sourceContent.split('\n').length;
// Find where the render function (template content) starts
const renderLineOffset = findRenderFunctionLine(wrappedCode);
if (renderLineOffset === 0) {
// Couldn't find render function, generate a basic mapping
console.warn('Could not find render function in wrapped output');
const mappings = new Array(outputLines).fill('AAAA').join(';');
const sourcemap = {
version: 3,
sources: [filename],
sourcesContent: [sourceContent],
mappings: mappings,
names: []
};
return Buffer.from(JSON.stringify(sourcemap)).toString('base64');
}
// Build mappings:
// 1. Lines before render content → all map to source line 1
// 2. Template content lines → map 1:1 to source lines
// 3. Lines after template content → all map to last source line
const mappings = [];
// Wrapper lines before template content
for (let i = 0; i < renderLineOffset - 1; i++) {
mappings.push('AAAA'); // Map to source line 1, column 0
}
// Template content lines (1:1 mapping)
// First line of template maps to source line 1
mappings.push('AAAA'); // Source line 1
// Remaining source lines map sequentially
for (let i = 1; i < sourceLines; i++) {
mappings.push('AACA'); // Each subsequent source line
}
// Any remaining wrapper lines after template content
const remainingLines = outputLines - mappings.length;
for (let i = 0; i < remainingLines; i++) {
mappings.push('AAAA'); // Map to last source line
}
// Create the sourcemap object
const sourcemap = {
version: 3,
sources: [filename],
sourcesContent: [sourceContent],
mappings: mappings.join(';'),
names: []
};
// Verify we have the right count
const finalCount = mappings.length;
if (finalCount !== outputLines) {
console.error(`Warning: Sourcemap line mismatch. Output has ${outputLines} lines, sourcemap has ${finalCount} mapping segments`);
}
return Buffer.from(JSON.stringify(sourcemap)).toString('base64');
}
/**
* Find the line number where template content starts in the wrapped output
* Returns 1-based line number
*/
function findRenderFunctionLine(outputCode) {
const lines = outputCode.split('\n');
for (let i = 0; i < lines.length; i++) {
// Look for the render function definition
if (lines[i].includes('render: function render(')) {
// Template content starts on the line AFTER the function declaration
// The function declaration ends with "{ let _output = []; ..."
// The next line is either empty or starts with template content
return i + 2; // i+1 for 1-based, +1 for next line after declaration
}
}
return 0; // Not found
}
//# sourceMappingURL=compiler.js.map