Add SPA session validation and buglist, update migration docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-03 21:28:08 +00:00
parent 9be3dfc14e
commit cff287e870
24169 changed files with 10223 additions and 7120 deletions

View File

@@ -0,0 +1 @@
../@jqhtml/parser/bin/jqhtml-compile

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 JQHTML Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,30 @@
# @jqhtml/parser
Compiles `.jqhtml` templates into JavaScript for the JQHTML component framework.
```bash
npx jqhtml-compile compile components/*.jqhtml -o bundle.js --sourcemap
```
Templates look like HTML with embedded JavaScript:
```html
<Define:Product_Card class="card">
<h3><%= this.data.name %></h3>
<% if (this.data.on_sale) { %>
<span class="badge">Sale!</span>
<% } %>
</Define:Product_Card>
```
The parser handles the template syntax - `<Define:>` blocks, `<%= %>` expressions, control flow, slots, event bindings. Output is JavaScript that the `@jqhtml/core` runtime understands.
## Status
Alpha release. Works well enough that I use it daily, but documentation is sparse and you might hit edge cases. Webpack loader and proper docs coming soon.
Found a bug or built something cool? Drop me a line.
---
**hansonxyz** · [hanson.xyz](https://hanson.xyz/) · [github](https://github.com/hansonxyz)

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env node
/**
* JQHTML Official CLI Compiler
*
* Compiles .jqhtml templates to JavaScript with multiple output formats
* Uses unified compiler module for single source of truth
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { compileTemplate } from '../dist/index.js';
import { readFileSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Get package version
const packagePath = path.join(path.dirname(__dirname), 'package.json');
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
const VERSION = packageJson.version;
// Parse command line arguments
function parseArgs(argv) {
const args = {
input: null,
output: null,
format: 'iife',
sourcemap: false,
statusJson: false,
help: false,
version: false
};
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--help' || arg === '-h') {
args.help = true;
} else if (arg === '--version' || arg === '-v') {
args.version = true;
} else if (arg === '--output' || arg === '-o') {
args.output = argv[++i];
if (!args.output) {
throw new Error('--output requires a file path');
}
} else if (arg === '--format') {
args.format = argv[++i];
if (!['iife', 'esm', 'cjs', 'umd'].includes(args.format)) {
throw new Error(`Invalid format: ${args.format}. Must be one of: iife, esm, cjs, umd`);
}
} else if (arg === '--sourcemap') {
args.sourcemap = true;
} else if (arg === '--status-json') {
args.statusJson = true;
} else if (!arg.startsWith('-')) {
args.input = arg;
}
}
return args;
}
// Show help message
function showHelp() {
console.log(`
JQHTML CLI Compiler v${VERSION}
Usage: jqhtml-compile <input.jqhtml> [options]
Options:
--output, -o <file> Write output to file instead of stdout
--format <format> Output format: iife|esm|cjs|umd (default: iife)
--sourcemap Include inline sourcemap
--status-json Output errors as JSON for programmatic parsing
--version, -v Show version number
--help, -h Show this help message
Examples:
jqhtml compile component.jqhtml
jqhtml compile component.jqhtml --output component.js
jqhtml compile component.jqhtml --format esm --sourcemap
jqhtml compile component.jqhtml -o dist/component.js --format umd
Output formats:
iife - Self-executing function that registers with window.jqhtml
esm - ES module export
cjs - CommonJS module.exports
umd - Universal module (works as AMD, CommonJS, or global)
`);
}
// Main compilation function
async function compile(inputFile, options) {
try {
// Read input file
const inputPath = path.resolve(inputFile);
if (!fs.existsSync(inputPath)) {
throw new Error(`Input file not found: ${inputPath}`);
}
const source = fs.readFileSync(inputPath, 'utf-8');
const filename = path.basename(inputPath);
// Use unified compiler
const compiled = compileTemplate(source, filename, {
format: options.format,
sourcemap: options.sourcemap,
version: VERSION
});
const formattedCode = compiled.code;
if (options.statusJson) {
// Output as JSON for programmatic consumption
const result = {
status: 'success',
result: formattedCode
};
if (options.output) {
result.outputFile = options.output;
}
console.log(JSON.stringify(result));
} else if (options.output) {
// Write to output file
const outputPath = path.resolve(options.output);
const outputDir = path.dirname(outputPath);
// Create output directory if it doesn't exist
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFileSync(outputPath, formattedCode, 'utf-8');
console.log(`✓ Compiled ${inputFile} → ${options.output}`);
} else {
// Output the compiled code to stdout without trailing newline
process.stdout.write(formattedCode);
}
return 0; // Success
} catch (error) {
if (options.statusJson) {
// Output error as JSON
const errorObj = {
status: 'error',
error: {
type: error.constructor.name,
message: error.message,
file: error.filename || null,
line: error.line || null,
column: error.column || null,
context: error.context || null,
suggestion: error.suggestion || null
}
};
console.log(JSON.stringify(errorObj));
} else {
// Output error to stderr
console.error(`Error: ${error.message}`);
if (error.line) {
console.error(` at line ${error.line}, column ${error.column || 0}`);
}
if (error.context) {
console.error(` context: ${error.context}`);
}
if (error.suggestion) {
console.error(` suggestion: ${error.suggestion}`);
}
}
return 1; // Error
}
}
// Main entry point
async function main() {
try {
const args = parseArgs(process.argv);
if (args.help) {
showHelp();
process.exit(0);
}
if (args.version) {
console.log(`v${VERSION}`);
process.exit(0);
}
if (!args.input) {
if (args.statusJson) {
console.log(JSON.stringify({
status: 'error',
error: {
type: 'ArgumentError',
message: 'No input file specified'
}
}));
} else {
console.error('Error: No input file specified');
console.error('Usage: jqhtml compile <input.jqhtml> [options]');
console.error('Use --help for more information');
}
process.exit(1);
}
const exitCode = await compile(args.input, args);
process.exit(exitCode);
} catch (error) {
console.error('Fatal error:', error.message);
process.exit(1);
}
}
// Run the CLI
main();

View File

@@ -0,0 +1,102 @@
import { SourceLocation } from './lexer.js';
export declare enum NodeType {
PROGRAM = "Program",
COMPONENT_DEFINITION = "ComponentDefinition",
COMPONENT_INVOCATION = "ComponentInvocation",
HTML_TAG = "HtmlTag",
TEXT = "Text",
EXPRESSION = "Expression",
IF_STATEMENT = "IfStatement",
FOR_STATEMENT = "ForStatement",
CODE_BLOCK = "CodeBlock",
FRAGMENT = "Fragment",
SLOT = "Slot",// v2 slot syntax
CONDITIONAL_ATTRIBUTE = "ConditionalAttribute"
}
export interface BaseNode {
type: NodeType;
start: number;
end: number;
line: number;
column: number;
loc?: SourceLocation;
range?: [number, number];
}
export interface ProgramNode extends BaseNode {
type: NodeType.PROGRAM;
body: ASTNode[];
}
export interface ComponentDefinitionNode extends BaseNode {
type: NodeType.COMPONENT_DEFINITION;
name: string;
body: ASTNode[];
attributes: Record<string, any>;
extends?: string;
defineArgs?: Record<string, any>;
isSlotOnly?: boolean;
slotNames?: string[];
}
export interface TextNode extends BaseNode {
type: NodeType.TEXT;
content: string;
}
export interface ExpressionNode extends BaseNode {
type: NodeType.EXPRESSION;
code: string;
escaped: boolean;
}
export interface IfStatementNode extends BaseNode {
type: NodeType.IF_STATEMENT;
condition: string;
consequent: ASTNode[];
alternate: ASTNode[] | null;
}
export interface ForStatementNode extends BaseNode {
type: NodeType.FOR_STATEMENT;
iterator: string;
body: ASTNode[];
}
export interface CodeBlockNode extends BaseNode {
type: NodeType.CODE_BLOCK;
tokens?: Array<{
type: string;
value: string;
}>;
code?: string;
}
export interface ConditionalAttributeNode extends BaseNode {
type: NodeType.CONDITIONAL_ATTRIBUTE;
condition: string;
attributes: Record<string, any>;
}
export interface FragmentNode extends BaseNode {
type: NodeType.FRAGMENT;
children: ASTNode[];
}
export interface SlotNode extends BaseNode {
type: NodeType.SLOT;
name: string;
attributes?: Record<string, any>;
children: ASTNode[];
selfClosing: boolean;
}
export interface ComponentInvocationNode extends BaseNode {
type: NodeType.COMPONENT_INVOCATION;
name: string;
attributes: Record<string, any>;
conditionalAttributes?: ConditionalAttributeNode[];
children: ASTNode[];
selfClosing: boolean;
}
export interface HtmlTagNode extends BaseNode {
type: NodeType.HTML_TAG;
name: string;
attributes: Record<string, any>;
conditionalAttributes?: ConditionalAttributeNode[];
children: ASTNode[];
selfClosing: boolean;
preserveWhitespace?: boolean;
}
export type ASTNode = ProgramNode | ComponentDefinitionNode | ComponentInvocationNode | HtmlTagNode | TextNode | ExpressionNode | IfStatementNode | ForStatementNode | CodeBlockNode | FragmentNode | SlotNode | ConditionalAttributeNode;
export declare function createNode<T extends ASTNode>(type: T['type'], props: Omit<T, 'type' | keyof BaseNode>, start: number, end: number, line: number, column: number, loc?: SourceLocation): T;
//# sourceMappingURL=ast.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,30 @@
// AST Node Types for JQHTML Templates
// Simple, clear node structures for the syntax tree
export var NodeType;
(function (NodeType) {
NodeType["PROGRAM"] = "Program";
NodeType["COMPONENT_DEFINITION"] = "ComponentDefinition";
NodeType["COMPONENT_INVOCATION"] = "ComponentInvocation";
NodeType["HTML_TAG"] = "HtmlTag";
NodeType["TEXT"] = "Text";
NodeType["EXPRESSION"] = "Expression";
NodeType["IF_STATEMENT"] = "IfStatement";
NodeType["FOR_STATEMENT"] = "ForStatement";
NodeType["CODE_BLOCK"] = "CodeBlock";
NodeType["FRAGMENT"] = "Fragment";
NodeType["SLOT"] = "Slot";
NodeType["CONDITIONAL_ATTRIBUTE"] = "ConditionalAttribute"; // Conditional attribute: <% if (cond) { %>attr="val"<% } %>
})(NodeType || (NodeType = {}));
// Helper to create nodes with common properties
export function createNode(type, props, start, end, line, column, loc) {
return {
type,
start,
end,
line,
column,
loc,
...props
};
}
//# sourceMappingURL=ast.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,oDAAoD;AAIpD,MAAM,CAAN,IAAY,QAaX;AAbD,WAAY,QAAQ;IAClB,+BAAmB,CAAA;IACnB,wDAA4C,CAAA;IAC5C,wDAA4C,CAAA;IAC5C,gCAAoB,CAAA;IACpB,yBAAa,CAAA;IACb,qCAAyB,CAAA;IACzB,wCAA4B,CAAA;IAC5B,0CAA8B,CAAA;IAC9B,oCAAwB,CAAA;IACxB,iCAAqB,CAAA;IACrB,yBAAa,CAAA;IACb,0DAA8C,CAAA,CAAE,4DAA4D;AAC9G,CAAC,EAbW,QAAQ,KAAR,QAAQ,QAanB;AA4HD,gDAAgD;AAChD,MAAM,UAAU,UAAU,CACxB,IAAe,EACf,KAAuC,EACvC,KAAa,EACb,GAAW,EACX,IAAY,EACZ,MAAc,EACd,GAAoB;IAEpB,OAAO;QACL,IAAI;QACJ,KAAK;QACL,GAAG;QACH,IAAI;QACJ,MAAM;QACN,GAAG;QACH,GAAG,KAAK;KACJ,CAAC;AACT,CAAC"}

View File

@@ -0,0 +1,108 @@
import { ProgramNode } from './ast.js';
export interface GeneratedCode {
code: string;
source_map?: string;
source_map_data_uri?: string;
components: Map<string, ComponentCode>;
}
export interface ComponentCode {
name: string;
render_function: string;
dependencies: string[];
tagName: string;
defaultAttributes: Record<string, any>;
defineArgs?: Record<string, any>;
extends?: string;
}
export declare class CodeGenerator {
private indent_level;
private components;
private current_component;
private in_slot;
private tag_depth;
private lastOutput;
private outputLine;
private outputColumn;
private sourceMapGenerator?;
private sourceContent?;
private sourceFile?;
private outputBuffer;
private enablePositionTracking;
private positionLog;
private currentOutputLine;
private preserveLines;
private outputLines;
private sourceLines;
generate(ast: ProgramNode, sourceFile?: string, sourceContent?: string): GeneratedCode;
/**
* Generate code with source maps using 1:1 line mapping + SourceMapGenerator
*/
generateWithSourceMap(ast: ProgramNode, sourceFile: string, sourceContent: string): GeneratedCode;
private generate_component_with_mappings_TESTING;
private generate_component;
private generate_function_body;
/**
* Generate function body with true 1:1 line mapping
* Each line in the source produces exactly one line in the output
*/
private generate_function_body_1to1;
private generate_node;
/**
* Padded trim: Collapse internal whitespace but preserve leading/trailing space
* Examples:
* " hello " → " hello "
* "hello" → "hello"
* " " → " "
* "\n\n \n" → " "
*/
private padded_trim;
private generate_text;
private generate_expression;
private generate_if;
private generate_for;
private generate_code_block;
private generate_tag_open;
private generate_html_tag;
private generate_component_invocation;
private parse_attributes;
private generate_attributes_with_conditionals;
private generate_attributes_object;
private is_self_closing_tag;
private compile_interpolated_value;
private escape_string;
private indent;
private extract_slots_from_children;
/**
* Emit text and track position for source maps
*/
private emit;
/**
* Emit a line of text (adds newline)
*/
private emitLine;
/**
* Reset position tracking
*/
private resetPositionTracking;
/**
* Get position tracking log for debugging
*/
getPositionLog(): Array<{
line: number;
column: number;
text: string;
node?: string;
}>;
/**
* Enable/disable position tracking
*/
setPositionTracking(enabled: boolean): void;
/**
* Serialize attribute object with proper handling of identifiers and expressions
* Quoted values become strings, identifiers/expressions become raw JavaScript
*/
private serializeAttributeObject;
private build_module_code;
}
export declare function generate(ast: ProgramNode, sourceFile?: string, sourceContent?: string): GeneratedCode;
//# sourceMappingURL=codegen.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"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"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
/**
* Unified JQHTML Compiler Module
*
* Single source of truth for compiling JQHTML templates to JavaScript
* with proper sourcemap generation and version injection.
*/
export interface CompileOptions {
format: 'iife' | 'esm' | 'cjs' | 'umd';
sourcemap: boolean;
version?: string;
}
export interface CompiledOutput {
code: string;
componentName: string;
}
/**
* 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 declare function compileTemplate(source: string, filename: string, options: CompileOptions): CompiledOutput;
//# sourceMappingURL=compiler.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,cAAc,GACtB,cAAc,CAmDhB"}

View File

@@ -0,0 +1,273 @@
/**
* 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_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':
// Self-executing function that auto-registers with window.jqhtml
output = `// Compiled from: ${componentName}.jqhtml
(function() {
'use strict';
const template_${name} = ${componentDef};
// Self-register with jqhtml runtime
// Must use window.jqhtml since we're in bundle scope
if (!window.jqhtml) {
throw new Error('FATAL: window.jqhtml is not defined. The jqhtml runtime must be loaded before registering templates.');
}
// Auto-register following standard jqhtml pattern
window.jqhtml.register_template(template_${name});
})();`;
break;
case 'esm':
// ES Module export with auto-registration
output = `// ES Module: ${name}
import jqhtml from '@jqhtml/core';
const template_${name} = ${componentDef};
// Auto-register following standard jqhtml pattern
jqhtml.register_template(template_${name});
export { template_${name} };
export default template_${name};`;
break;
case 'cjs':
// CommonJS export with auto-registration
output = `// CommonJS Module: ${name}
'use strict';
const template_${name} = ${componentDef};
// Auto-register if jqhtml is available
if (typeof window !== 'undefined' && window.jqhtml) {
window.jqhtml.register_template(template_${name});
}
module.exports = template_${name};
module.exports.default = template_${name};
module.exports.template_${name} = template_${name};`;
break;
case 'umd':
// Universal Module Definition with auto-registration
output = `(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['@jqhtml/core'], 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 (jqhtml) {
'use strict';
const template_${name} = ${componentDef};
// Auto-register with jqhtml runtime
if (typeof window !== 'undefined' && window.jqhtml) {
window.jqhtml.register_template(template_${name});
} else if (jqhtml) {
jqhtml.register_template(template_${name});
}
return template_${name};
}));`;
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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
export declare class JQHTMLParseError extends Error {
line: number;
column: number;
endLine?: number;
endColumn?: number;
source?: string;
filename?: string;
severity: 'error' | 'warning';
suggestion?: string;
constructor(message: string, line: number, column: number, source?: string, filename?: string);
private buildErrorMessage;
private getCodeSnippet;
}
export declare function unclosedError(type: string, name: string, line: number, column: number, source?: string, filename?: string): JQHTMLParseError;
export declare function mismatchedTagError(opening: string, closing: string, line: number, column: number, source?: string, filename?: string): JQHTMLParseError;
export declare function syntaxError(message: string, line: number, column: number, source?: string, filename?: string): JQHTMLParseError;
export declare function getSuggestion(error: string): string;
export declare class ErrorCollector {
private errors;
private maxErrors;
constructor(maxErrors?: number);
add(error: JQHTMLParseError): void;
hasErrors(): boolean;
getErrors(): JQHTMLParseError[];
throwIfErrors(): void;
}
//# sourceMappingURL=errors.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAGA,qBAAa,gBAAiB,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAW;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;gBAGzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;IAgBnB,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,cAAc;CAiCvB;AAGD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,gBAAgB,CAQlB;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,gBAAgB,CAQlB;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,gBAAgB,CAQlB;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiCnD;AAGD,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,SAAS,CAAc;gBAEnB,SAAS,GAAE,MAAW;IAIlC,GAAG,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAMlC,SAAS,IAAI,OAAO;IAIpB,SAAS,IAAI,gBAAgB,EAAE;IAI/B,aAAa,IAAI,IAAI;CAmBtB"}

View File

@@ -0,0 +1,155 @@
// JQHTML Parser Error Classes
// Provides helpful error messages with code context
export class JQHTMLParseError extends Error {
line;
column;
endLine;
endColumn;
source;
filename;
severity = 'error';
suggestion;
constructor(message, line, column, source, filename) {
super(message);
this.name = 'JQHTMLParseError';
this.line = line;
this.column = column;
this.source = source;
this.filename = filename;
// Auto-add suggestions
this.suggestion = getSuggestion(message);
// Build the full error message with context
this.message = this.buildErrorMessage(message);
}
buildErrorMessage(message) {
let result = message;
// Add location info IMMEDIATELY after message (required for RSpade parsing)
// Format: "at filename:line:column" (RSpade regex: /at [^:]+:(\d+):(\d+)/i)
if (this.filename) {
result += `\nat ${this.filename}:${this.line}:${this.column}`;
}
else {
// Fallback to generic format if no filename
result += `\nat <unknown>:${this.line}:${this.column}`;
}
// Add code snippet if source is available
if (this.source) {
const snippet = this.getCodeSnippet();
if (snippet) {
result += '\n\n' + snippet;
}
}
// Add suggestion AFTER code snippet (don't interfere with parsing)
if (this.suggestion) {
result += this.suggestion;
}
return result;
}
getCodeSnippet() {
if (!this.source)
return '';
const lines = this.source.split('\n');
const lineIndex = this.line - 1;
// Show 3 lines before and after for better context
const contextLines = 3;
const startLine = Math.max(0, lineIndex - contextLines);
const endLine = Math.min(lines.length - 1, lineIndex + contextLines);
let snippet = '';
for (let i = startLine; i <= endLine; i++) {
const lineNum = i + 1;
const isErrorLine = i === lineIndex;
const prefix = isErrorLine ? '>' : ' ';
// Line number with padding
const lineNumStr = String(lineNum).padStart(5, ' ');
snippet += `${prefix} ${lineNumStr} | ${lines[i]}\n`;
// Add pointer to error column with better highlighting
if (isErrorLine) {
const spaces = ' '.repeat(this.column + 8);
const carets = '^'.repeat(Math.min(lines[i].length - this.column + 1, 20));
snippet += `${spaces}${carets}\n`;
}
}
return snippet;
}
}
// Common error factories
export function unclosedError(type, name, line, column, source, filename) {
return new JQHTMLParseError(`Unclosed ${type}: ${name}`, line, column, source, filename);
}
export function mismatchedTagError(opening, closing, line, column, source, filename) {
return new JQHTMLParseError(`Mismatched tags: expected </${opening}>, found </${closing}>`, line, column, source, filename);
}
export function syntaxError(message, line, column, source, filename) {
return new JQHTMLParseError(`Syntax error: ${message}`, line, column, source, filename);
}
// Helpful suggestions for common mistakes
export function getSuggestion(error) {
if (error.includes('Unclosed if statement')) {
return '\nDid you forget the closing <% } %>?';
}
if (error.includes('Unclosed for statement')) {
return '\nDid you forget the closing <% } %>?';
}
if (error.includes('Unclosed component definition')) {
return '\nDid you forget the closing </Define:ComponentName> tag?';
}
if (error.includes('Unclosed slot')) {
return '\nDid you mean to use a self-closing slot? Try <Slot:name /> instead.';
}
if (error.includes('Unclosed tag') || error.includes('Unclosed component')) {
return '\n\nThis element was opened but never closed. Make sure every opening tag has a matching closing tag.\n' +
'Common causes:\n' +
' • Missing closing tag (e.g., forgot </div>)\n' +
' • Tag opened in if/for block but closed outside (split tag conditional - not allowed)\n' +
' • Mismatched nesting (e.g., <div><span></div></span>)\n\n' +
'If opened in <% if/for %>, it MUST close in the same block:\n' +
' ❌ <% if (x) { %> <div> <% } %> </div>\n' +
' ✅ <% if (x) { %> <div>...</div> <% } %>';
}
if (error.includes('Expected %>')) {
return '\nCheck if you have a %> inside a string literal. Escape it or use a different approach.';
}
if (error.includes('Mismatched tags')) {
return '\nCheck that your opening and closing tags match exactly (case-sensitive).';
}
if (error.includes('Mixed content not allowed')) {
return '\nWhen using slots, wrap all content in <Slot:slotname> tags. Use <Slot:default> for the main content.';
}
return '';
}
// Error collection for batch reporting
export class ErrorCollector {
errors = [];
maxErrors = 10;
constructor(maxErrors = 10) {
this.maxErrors = maxErrors;
}
add(error) {
if (this.errors.length < this.maxErrors) {
this.errors.push(error);
}
}
hasErrors() {
return this.errors.length > 0;
}
getErrors() {
return this.errors;
}
throwIfErrors() {
if (this.errors.length === 0)
return;
if (this.errors.length === 1) {
throw this.errors[0];
}
// Multiple errors - create a combined message
let message = `Found ${this.errors.length} errors:\n\n`;
this.errors.forEach((error, index) => {
message += `Error ${index + 1}: ${error.message}\n`;
if (index < this.errors.length - 1) {
message += '\n';
}
});
throw new Error(message);
}
}
//# sourceMappingURL=errors.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,8 @@
export { Lexer, Token, TokenType } from './lexer.js';
export { Parser } from './parser.js';
export { CodeGenerator, generate } from './codegen.js';
export { compileTemplate, CompileOptions, CompiledOutput } from './compiler.js';
export { NodeType, ASTNode, ProgramNode, ComponentDefinitionNode, TextNode, ExpressionNode, IfStatementNode, ForStatementNode, CodeBlockNode, FragmentNode } from './ast.js';
export { JQHTMLParseError, ErrorCollector } from './errors.js';
export declare function parse(source: string, filename?: string): import("./ast.js").ProgramNode;
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EACL,QAAQ,EACR,OAAO,EACP,WAAW,EACX,uBAAuB,EACvB,QAAQ,EACR,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,YAAY,EACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG/D,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,kCAoBtD"}

View File

@@ -0,0 +1,31 @@
// JQHTML Parser - Main entry point
import { Lexer } from './lexer.js';
import { Parser } from './parser.js';
export { Lexer, TokenType } from './lexer.js';
export { Parser } from './parser.js';
export { CodeGenerator, generate } from './codegen.js';
export { compileTemplate } from './compiler.js';
export { NodeType } from './ast.js';
export { JQHTMLParseError, ErrorCollector } from './errors.js';
// Convenience function for parsing with error context
export function parse(source, filename) {
try {
const lexer = new Lexer(source);
const tokens = lexer.tokenize();
const parser = new Parser(tokens, source, filename);
return parser.parse();
}
catch (error) {
// If it's already a JQHTMLParseError, just re-throw it
if (error.name === 'JQHTMLParseError') {
throw error;
}
// Otherwise wrap it with context
const message = error.message || String(error);
const enhancedMessage = filename
? `Error parsing ${filename}: ${message}`
: `Error parsing JQHTML: ${message}`;
throw new Error(enhancedMessage);
}
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,KAAK,EAAS,SAAS,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,eAAe,EAAkC,MAAM,eAAe,CAAC;AAChF,OAAO,EACL,QAAQ,EAUT,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE/D,sDAAsD;AACtD,MAAM,UAAU,KAAK,CAAC,MAAc,EAAE,QAAiB;IACrD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;QACpD,OAAO,MAAM,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,uDAAuD;QACvD,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;YACtC,MAAM,KAAK,CAAC;QACd,CAAC;QAED,iCAAiC;QACjC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,eAAe,GAAG,QAAQ;YAC9B,CAAC,CAAC,iBAAiB,QAAQ,KAAK,OAAO,EAAE;YACzC,CAAC,CAAC,yBAAyB,OAAO,EAAE,CAAC;QAEvC,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACnC,CAAC;AACH,CAAC"}

View File

@@ -0,0 +1,3 @@
export declare function with_template(ComponentClass: any, template_name: string): any;
export declare function register_compiled_templates(template_map: Map<string, any>): void;
//# sourceMappingURL=integration.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAkBA,wBAAgB,aAAa,CAAC,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,OA8BvE;AAGD,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,QASzE"}

View File

@@ -0,0 +1,47 @@
// JQHTML v2 Template Integration
// Connects compiled templates to Component class
/// <reference path="./types.d.ts" />
import { process_instructions, html, register_template } from './runtime.js';
// Mixin to add template support to any component
export function with_template(ComponentClass, template_name) {
const original_on_render = ComponentClass.prototype.on_render;
ComponentClass.prototype.on_render = async function () {
// Get compiled template from registry
const global_obj = typeof window !== 'undefined' ? window : global;
const template_map = global_obj.jqhtml_components;
if (!template_map || !template_map.has(template_name)) {
throw new Error(`Template not found: ${template_name}`);
}
const template_info = template_map.get(template_name);
const render_fn = template_info.render;
// Call render function with component context
const [instructions] = render_fn.call(this, this.constructor, this.data, this.args, {});
// Process instructions into DOM
const rendered = process_instructions(instructions, this);
// Clear and append to component element
this.$.empty().append(rendered);
// Call original on_render if it exists
if (original_on_render) {
await original_on_render.call(this);
}
};
return ComponentClass;
}
// Register templates from compiled module
export function register_compiled_templates(template_map) {
// Store on global object for now (MVP approach)
const global_obj = typeof window !== 'undefined' ? window : global;
global_obj.jqhtml_components = template_map;
// Also register each template
for (const [name, info] of template_map) {
register_template(name, info.render);
}
}
// Make html function available globally for templates
if (typeof window !== 'undefined') {
window.html = html;
}
else if (typeof global !== 'undefined') {
global.html = html;
}
//# sourceMappingURL=integration.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,iDAAiD;AAEjD,qCAAqC;AAErC,OAAO,EAAE,oBAAoB,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAY7E,iDAAiD;AACjD,MAAM,UAAU,aAAa,CAAC,cAAmB,EAAE,aAAqB;IACtE,MAAM,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,SAAS,CAAC;IAE9D,cAAc,CAAC,SAAS,CAAC,SAAS,GAAG,KAAK;QACxC,sCAAsC;QACtC,MAAM,UAAU,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACnE,MAAM,YAAY,GAAI,UAAkB,CAAC,iBAAiB,CAAC;QAC3D,IAAI,CAAC,YAAY,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,KAAK,CAAC,uBAAuB,aAAa,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACtD,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC;QAEvC,8CAA8C;QAC9C,MAAM,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAExF,gCAAgC;QAChC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QAE1D,wCAAwC;QACxC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAEhC,uCAAuC;QACvC,IAAI,kBAAkB,EAAE,CAAC;YACvB,MAAM,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,2BAA2B,CAAC,YAA8B;IACxE,gDAAgD;IAChD,MAAM,UAAU,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAClE,UAAkB,CAAC,iBAAiB,GAAG,YAAY,CAAC;IAErD,8BAA8B;IAC9B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,YAAY,EAAE,CAAC;QACxC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;AACH,CAAC;AAED,sDAAsD;AACtD,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;IAClC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;AACrB,CAAC;KAAM,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;IACxC,MAAc,CAAC,IAAI,GAAG,IAAI,CAAC;AAC9B,CAAC"}

View File

@@ -0,0 +1,123 @@
export declare enum TokenType {
TEXT = "TEXT",
EXPRESSION_START = "EXPRESSION_START",// <%=
EXPRESSION_UNESCAPED = "EXPRESSION_UNESCAPED",// <%!=
CODE_START = "CODE_START",// <%
TAG_END = "TAG_END",// %>
COMMENT = "COMMENT",// <%-- comment --%>
DEFINE_START = "DEFINE_START",// <Define:
DEFINE_END = "DEFINE_END",// </Define:
COMPONENT_NAME = "COMPONENT_NAME",
SLOT_START = "SLOT_START",// <Slot:
SLOT_END = "SLOT_END",// </Slot:
SLOT_NAME = "SLOT_NAME",
TAG_OPEN = "TAG_OPEN",// <tagname or <ComponentName
TAG_CLOSE = "TAG_CLOSE",// </tagname or </ComponentName
TAG_NAME = "TAG_NAME",// The tag/component name
SELF_CLOSING = "SELF_CLOSING",// />
ATTR_NAME = "ATTR_NAME",// $property or regular
ATTR_VALUE = "ATTR_VALUE",
COLON = "COLON",
SEMICOLON = "SEMICOLON",
GT = "GT",// >
LT = "LT",// <
SLASH = "SLASH",// /
EQUALS = "EQUALS",// =
QUOTE = "QUOTE",// " or '
EOF = "EOF",
NEWLINE = "NEWLINE",
WHITESPACE = "WHITESPACE",
JAVASCRIPT = "JAVASCRIPT"
}
export interface SourceLocation {
start: {
line: number;
column: number;
offset: number;
};
end: {
line: number;
column: number;
offset: number;
};
}
export interface Token {
type: TokenType;
value: string;
line: number;
column: number;
start: number;
end: number;
loc?: SourceLocation;
}
export declare class Lexer {
private input;
private position;
private line;
private column;
private tokens;
private savedPosition;
constructor(input: string);
/**
* Save current position for later token creation
*/
private savePosition;
/**
* Get saved position or current position
*/
private getSavedPosition;
/**
* Replace <%-- comment --%> with equivalent number of newlines
* This ensures line mapping stays accurate while removing comment content
*/
private preprocessComments;
/**
* Replace HTML comments (<!-- -->) that appear OUTSIDE of <Define> tags
* This strips documentation comments before component definitions
* HTML comments INSIDE <Define> tags are preserved in the output
*/
private preprocessHTMLComments;
/**
* Preprocess code blocks and expressions
* - Insert comment markers for empty lines in code blocks
* - Collapse multi-line expressions to single line with trailing newlines
* This ensures 1:1 line mapping in generated code
*/
private preprocessCodeBlocks;
/**
* Find the closing %> tag, properly handling strings and comments
*/
private findClosingTag;
tokenize(): Token[];
private scan_next;
private scan_text;
private scan_code_block;
private scan_comment;
private scan_html_comment;
private scan_expression;
private scan_javascript;
private scan_component_name;
private scan_slot_name;
private match_sequence;
private match_keyword;
private peek_sequence;
private peek_sequence_at;
private skip_whitespace;
private current_char;
private peek_ahead;
private advance;
private add_token;
private is_tag_name_char;
private is_tag_name_continue_char;
private scan_opening_tag;
private scan_closing_tag;
private scan_attributes;
private is_attribute_start_char;
private scan_attribute;
private scan_attribute_value;
private validate_unquoted_value;
private get_current_attribute_context;
private value_contains_interpolation;
private scan_interpolated_attribute_value;
}
//# sourceMappingURL=lexer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"lexer.d.ts","sourceRoot":"","sources":["../src/lexer.ts"],"names":[],"mappings":"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"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
import { Token } from './lexer.js';
import { ProgramNode } from './ast.js';
export declare class Parser {
private tokens;
private current;
private source?;
private filename?;
private static readonly VOID_ELEMENTS;
constructor(tokens: Token[], source?: string, filename?: string);
/**
* Validate JavaScript code for common mistakes
*/
private validate_javascript_code;
parse(): ProgramNode;
private parse_top_level;
private parse_component_definition;
private parse_content;
private parse_expression;
private parse_code_block;
private static readonly JAVASCRIPT_RESERVED_WORDS;
private parse_slot;
private parse_tag;
private parse_attributes;
private parse_conditional_attribute;
private parse_attribute_value;
private check_closing_tag;
private match;
private check;
private check_ahead;
private check_sequence;
private advance;
private is_at_end;
private peek;
private peek_ahead;
private previous;
private current_token;
private previous_token;
/**
* Create a SourceLocation from start and end tokens
* Propagates loc field if available, falls back to old fields for compatibility
*/
private create_location;
private consume;
private validate_component_children;
/**
* Compile method for simplified API
* Parses the template and returns component metadata and render function
*/
compile(): {
name: string;
tagName: string;
defaultAttributes: Record<string, any>;
renderFunction: string;
};
}
//# sourceMappingURL=parser.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAA6B,MAAM,YAAY,CAAC;AAC9D,OAAO,EAGL,WAAW,EAUZ,MAAM,UAAU,CAAC;AAUlB,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAI1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAGlC;gBAES,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;IAM/D;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA0BhC,KAAK,IAAI,WAAW;IA0EpB,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,0BAA0B;IAqNlC,OAAO,CAAC,aAAa;IA6DrB,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,gBAAgB;IAyCxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAW9C;IAGH,OAAO,CAAC,UAAU;IAmGlB,OAAO,CAAC,SAAS;IAgNjB,OAAO,CAAC,gBAAgB;IA+ExB,OAAO,CAAC,2BAA2B;IA4FnC,OAAO,CAAC,qBAAqB;IA2E7B,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,KAAK;IAUb,OAAO,CAAC,KAAK;IAKb,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;IAItB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,OAAO;IA4Cf,OAAO,CAAC,2BAA2B;IA+BnC;;;OAGG;IACH,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE;CAkC7G"}

View File

@@ -0,0 +1,845 @@
// JQHTML Parser - Builds AST from tokens
// Simple recursive descent parser, no complex libraries
import { TokenType } from './lexer.js';
import { NodeType, createNode } from './ast.js';
import { JQHTMLParseError, unclosedError, mismatchedTagError, syntaxError, getSuggestion } from './errors.js';
import { CodeGenerator } from './codegen.js';
export class Parser {
tokens;
current = 0;
source;
filename;
// HTML5 void elements that cannot have closing tags
// These are automatically treated as self-closing
static VOID_ELEMENTS = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'link', 'meta', 'source', 'track', 'wbr'
]);
constructor(tokens, source, filename) {
this.tokens = tokens;
this.source = source;
this.filename = filename;
}
/**
* Validate JavaScript code for common mistakes
*/
validate_javascript_code(code, token) {
// Check for this.innerHTML usage (should use content() function instead)
if (/\bthis\.innerHTML\b/.test(code)) {
const error = new JQHTMLParseError(`Invalid usage: this.innerHTML is not available in JQHTML templates.\n` +
`Did you mean to use the content() function instead?`, token.line, token.column, this.source);
error.suggestion =
`\nJQHTML uses content() to render child elements, not this.innerHTML:\n\n` +
` ❌ Wrong: <%= this.innerHTML %>\n` +
` ✓ Correct: <%= content() %>\n\n` +
` ❌ Wrong: <% if (condition) { %> this.innerHTML <% } %>\n` +
` ✓ Correct: <% if (condition) { %> <%= content() %> <% } %>\n\n` +
`Why content() instead of this.innerHTML?\n` +
`- content() is a function that returns the rendered child content\n` +
`- this.innerHTML is a DOM property, not available during template compilation\n` +
`- content() supports named slots: content('slot_name')\n` +
`- content() can pass data: content('row', rowData)`;
throw error;
}
}
// Main entry point - parse tokens into AST
parse() {
const body = [];
const start = this.current_token();
// Skip leading whitespace and newlines
while (!this.is_at_end() && (this.match(TokenType.NEWLINE) ||
(this.match(TokenType.TEXT) && this.previous_token().value.trim() === ''))) {
// Skip whitespace
}
// Check if file is empty (only whitespace)
if (this.is_at_end()) {
// Empty file is allowed
return createNode(NodeType.PROGRAM, { body: [] }, start.start, start.end, start.line, start.column, this.create_location(start, start));
}
// Must have exactly one Define tag at top level
if (!this.check(TokenType.DEFINE_START)) {
const token = this.current_token();
throw syntaxError('JQHTML files must have exactly one top-level <Define:ComponentName> tag', token.line, token.column, this.source, this.filename);
}
// Parse the single component definition
const component = this.parse_component_definition();
if (component) {
body.push(component);
}
// Skip trailing whitespace and newlines
while (!this.is_at_end() && (this.match(TokenType.NEWLINE) || this.match(TokenType.TEXT) && this.previous_token().value.trim() === '')) {
// Skip whitespace
}
// Ensure no other content after the Define tag
if (!this.is_at_end()) {
const token = this.current_token();
throw syntaxError('JQHTML files must have exactly one top-level <Define:ComponentName> tag. Found additional content after the component definition.', token.line, token.column, this.source, this.filename);
}
const end = this.previous_token();
return createNode(NodeType.PROGRAM, { body }, start.start, end.end, start.line, start.column, this.create_location(start, end));
}
// Parse top-level constructs
parse_top_level() {
// Skip whitespace-only text nodes at top level
if (this.match(TokenType.NEWLINE)) {
return null;
}
// Component definition
if (this.check(TokenType.DEFINE_START)) {
return this.parse_component_definition();
}
// Regular content
return this.parse_content();
}
// Parse <Define:ComponentName>...</Define:ComponentName>
parse_component_definition() {
const start_token = this.consume(TokenType.DEFINE_START, 'Expected <Define:');
const name_token = this.consume(TokenType.COMPONENT_NAME, 'Expected component name');
// Validate component name starts with capital letter
if (!/^[A-Z]/.test(name_token.value)) {
throw syntaxError(`Component name '${name_token.value}' must start with a capital letter. Convention is First_Letter_With_Underscores.`, name_token.line, name_token.column, this.source, this.filename);
}
// Parse attributes (like tag="span", class="card", extends="Parent", $prop=value)
const attributes = {};
const defineArgs = {}; // $ attributes (raw JS, not data- attributes)
let extendsValue;
// Skip any leading newlines before attributes
while (this.match(TokenType.NEWLINE)) {
// Skip
}
while (!this.check(TokenType.GT) && !this.is_at_end()) {
const attr_name = this.consume(TokenType.ATTR_NAME, 'Expected attribute name');
// Validate that $sid is not used in Define tags
if (attr_name.value === '$sid') {
throw syntaxError('$sid is not allowed in <Define:> tags. Component definitions cannot have scoped IDs.', attr_name.line, attr_name.column, this.source, this.filename);
}
this.consume(TokenType.EQUALS, 'Expected =');
const attr_value = this.parse_attribute_value();
// Check if attribute value contains template expressions (dynamic content)
// Define tag attributes must be static - they're part of the template definition, not runtime values
const hasInterpolation = attr_value &&
typeof attr_value === 'object' &&
(attr_value.interpolated === true ||
(attr_value.parts && attr_value.parts.some((p) => p.type === 'expression')));
if (hasInterpolation) {
// Special error message for class attribute - most common case
if (attr_name.value === 'class') {
const error = syntaxError(`Template expressions cannot be used in <Define:> tag attributes. The <Define> tag is a static template definition, not a live component instance.`, attr_name.line, attr_name.column, this.source, this.filename);
error.message += '\n\n' +
' For dynamic classes, set the class attribute on the component invocation instead:\n' +
' ❌ <Define:MyComponent class="<%= this.args.col_class || \'default\' %>">\n' +
' ✅ <Define:MyComponent class="static-class">\n' +
' ...\n' +
' </Define:MyComponent>\n\n' +
' Then when using the component:\n' +
' <MyComponent class="col-md-8 col-xl-4" />\n\n' +
' Or set attributes dynamically in on_create() or on_ready():\n' +
' on_ready() {\n' +
' const classes = this.args.col_class || \'col-12 col-md-6\';\n' +
' this.$.addClass(classes);\n' +
' }';
throw error;
}
else {
// General error for other attributes
const error = syntaxError(`Template expressions cannot be used in <Define:> tag attributes. The <Define> tag is a static template definition, not a live component instance.`, attr_name.line, attr_name.column, this.source, this.filename);
error.message += '\n\n' +
` For dynamic "${attr_name.value}" attribute values, set them dynamically in lifecycle methods:\n` +
' ❌ <Define:MyComponent ' + attr_name.value + '="<%= expression %>">\n' +
' ✅ <Define:MyComponent>\n' +
' ...\n' +
' </Define:MyComponent>\n\n' +
' Then in your component class:\n' +
' on_create() {\n' +
` this.$.attr('${attr_name.value}', this.args.some_value);\n` +
' }\n\n' +
' Or use on_ready() for attributes that need DOM to be fully initialized.';
throw error;
}
}
// Handle special attributes on Define tags
if (attr_name.value === 'extends') {
// extends="ParentComponent" - explicit template inheritance
if (typeof attr_value === 'object' && attr_value.quoted) {
extendsValue = attr_value.value;
}
else if (typeof attr_value === 'string') {
extendsValue = attr_value;
}
else {
throw syntaxError(`extends attribute must be a quoted string with the parent component name`, attr_name.line, attr_name.column, this.source, this.filename);
}
}
else if (attr_name.value.startsWith('$')) {
// $ attributes on Define tags are raw JS assignments (like component invocations)
// Store them separately - they don't become data- attributes
const propName = attr_name.value.substring(1); // Remove $
defineArgs[propName] = attr_value;
}
else {
// Regular attributes (tag="span", class="card", etc.)
attributes[attr_name.value] = attr_value;
}
// Skip newlines between attributes
while (this.match(TokenType.NEWLINE)) {
// Skip
}
}
this.consume(TokenType.GT, 'Expected >');
const body = [];
// Parse until we find the closing tag
while (!this.check(TokenType.DEFINE_END)) {
if (this.is_at_end()) {
const error = unclosedError('component definition', name_token.value, name_token.line, name_token.column, this.source, this.filename);
error.message += getSuggestion(error.message);
throw error;
}
const node = this.parse_content();
if (node) {
body.push(node);
}
}
// Consume closing tag
this.consume(TokenType.DEFINE_END, 'Expected </Define:');
const closing_name = this.consume(TokenType.COMPONENT_NAME, 'Expected component name');
if (closing_name.value !== name_token.value) {
throw mismatchedTagError(`Define:${name_token.value}`, `Define:${closing_name.value}`, closing_name.line, closing_name.column, this.source, this.filename);
}
const end_token = this.consume(TokenType.GT, 'Expected >');
// Detect slot-only templates for inheritance
let isSlotOnly = false;
let slotNames = [];
// Check if body contains only slot nodes (ignoring whitespace-only text)
const nonWhitespaceNodes = body.filter(node => {
if (node.type === NodeType.TEXT) {
return node.content.trim() !== '';
}
return true;
});
if (nonWhitespaceNodes.length > 0) {
// Check if ALL non-whitespace nodes are slots
const allSlots = nonWhitespaceNodes.every(node => node.type === NodeType.SLOT);
if (allSlots) {
isSlotOnly = true;
slotNames = nonWhitespaceNodes.map(node => node.name);
}
}
return createNode(NodeType.COMPONENT_DEFINITION, {
name: name_token.value,
body,
attributes,
extends: extendsValue,
defineArgs: Object.keys(defineArgs).length > 0 ? defineArgs : undefined,
isSlotOnly,
slotNames
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
// Parse content (text, expressions, control flow)
parse_content() {
// Comments are now preprocessed into whitespace by the lexer
// Plain text
if (this.match(TokenType.TEXT)) {
const token = this.previous();
return createNode(NodeType.TEXT, { content: token.value }, token.start, token.end, token.line, token.column, this.create_location(token, token));
}
// Expression <%= ... %> or <%!= ... %>
if (this.match(TokenType.EXPRESSION_START) ||
this.match(TokenType.EXPRESSION_UNESCAPED)) {
return this.parse_expression();
}
// Code block <% ... %>
if (this.match(TokenType.CODE_START)) {
return this.parse_code_block();
}
// Slot <Slot:name>...</Slot:name>
if (this.match(TokenType.SLOT_START)) {
return this.parse_slot();
}
// HTML tags and component invocations
if (this.match(TokenType.TAG_OPEN)) {
return this.parse_tag();
}
// Skip newlines in content
if (this.match(TokenType.NEWLINE)) {
const token = this.previous();
return createNode(NodeType.TEXT, { content: token.value }, token.start, token.end, token.line, token.column, this.create_location(token, token));
}
// Advance if we don't recognize the token
if (!this.is_at_end()) {
this.advance();
}
return null;
}
// Parse <%= expression %> or <%!= expression %>
parse_expression() {
const start_token = this.previous(); // EXPRESSION_START or EXPRESSION_UNESCAPED
const code_token = this.consume(TokenType.JAVASCRIPT, 'Expected JavaScript code');
// Validate JavaScript code for common mistakes
this.validate_javascript_code(code_token.value, code_token);
const end_token = this.consume(TokenType.TAG_END, 'Expected %>');
return createNode(NodeType.EXPRESSION, {
code: code_token.value,
escaped: start_token.type === TokenType.EXPRESSION_START
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
// Parse <% code %> - collect tokens with their types for proper transformation
parse_code_block() {
const start_token = this.previous(); // CODE_START
// Collect tokens with their types
const tokens = [];
while (!this.check(TokenType.TAG_END)) {
if (this.is_at_end()) {
throw syntaxError('Unterminated code block - expected %>', start_token.line, start_token.column, this.source, this.filename);
}
const token = this.advance();
// Validate JavaScript tokens for common mistakes
if (token.type === TokenType.JAVASCRIPT) {
this.validate_javascript_code(token.value, token);
}
tokens.push({ type: token.type, value: token.value });
}
const end_token = this.consume(TokenType.TAG_END, 'Expected %>');
return createNode(NodeType.CODE_BLOCK, { tokens }, // Pass tokens array instead of concatenated code
start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
// JavaScript reserved words that cannot be used as slot names
static JAVASCRIPT_RESERVED_WORDS = new Set([
// Keywords
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally',
'for', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'null',
'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
'var', 'void', 'while', 'with', 'yield',
// Future reserved words
'implements', 'interface', 'package', 'private', 'protected', 'public', 'static', 'await',
// Other problematic words
'arguments', 'eval'
]);
// Parse slot <Slot:name>content</Slot:name> or <Slot:name />
parse_slot() {
const start_token = this.previous(); // SLOT_START
const name_token = this.consume(TokenType.SLOT_NAME, 'Expected slot name');
// Validate slot name against JavaScript reserved words
if (Parser.JAVASCRIPT_RESERVED_WORDS.has(name_token.value.toLowerCase())) {
throw syntaxError(`Slot name "${name_token.value}" is a JavaScript reserved word and cannot be used. Please choose a different name.`, name_token.line, name_token.column, this.source, this.filename);
}
// TODO: Parse attributes for let:prop syntax in future
const attributes = {};
// Check for self-closing slot
if (this.match(TokenType.SLASH)) {
const end_token = this.consume(TokenType.GT, 'Expected >');
return createNode(NodeType.SLOT, {
name: name_token.value,
attributes,
children: [],
selfClosing: true
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
// Regular slot with content
this.consume(TokenType.GT, 'Expected >');
const children = [];
// Parse until we find the closing tag
while (!this.check(TokenType.SLOT_END)) {
if (this.is_at_end()) {
const error = unclosedError('slot', name_token.value, name_token.line, name_token.column, this.source, this.filename);
error.message += getSuggestion(error.message);
throw error;
}
const node = this.parse_content();
if (node) {
children.push(node);
}
}
// Consume closing tag
this.consume(TokenType.SLOT_END, 'Expected </Slot:');
const closing_name = this.consume(TokenType.SLOT_NAME, 'Expected slot name');
if (closing_name.value !== name_token.value) {
throw mismatchedTagError(name_token.value, closing_name.value, closing_name.line, closing_name.column, this.source, this.filename);
}
const end_token = this.consume(TokenType.GT, 'Expected >');
return createNode(NodeType.SLOT, {
name: name_token.value,
attributes,
children,
selfClosing: false
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
// Token navigation helpers
// Parse HTML tag or component invocation
parse_tag() {
const start_token = this.previous(); // TAG_OPEN
const name_token = this.consume(TokenType.TAG_NAME, 'Expected tag name');
let tag_name = name_token.value;
let original_tag_name = null; // Track original for $redrawable
// Check for forbidden tags
const tag_lower = tag_name.toLowerCase();
if (tag_lower === 'script' || tag_lower === 'style') {
throw syntaxError(`<${tag_name}> tags are not allowed in JQHTML templates. ` +
`Use external files or inline styles via attributes instead.`, name_token.line, name_token.column, this.source, this.filename);
}
// Determine if this is a component (starts with capital letter) or HTML tag
let is_component = tag_name[0] >= 'A' && tag_name[0] <= 'Z';
// Check if this is an HTML5 void element (only for HTML tags, not components)
const is_void_element = !is_component && Parser.VOID_ELEMENTS.has(tag_lower);
// Parse attributes
const { attributes, conditionalAttributes } = this.parse_attributes();
// Check for $redrawable attribute transformation
// Transform <div $redrawable> to <Redrawable tag="div">
if (attributes['$redrawable'] !== undefined || attributes['data-redrawable'] !== undefined) {
const redrawable_attr = attributes['$redrawable'] !== undefined ? '$redrawable' : 'data-redrawable';
// Remove the $redrawable attribute
delete attributes[redrawable_attr];
// Store original tag name for closing tag matching
original_tag_name = tag_name;
// Add tag="original_tag_name" attribute
attributes['tag'] = { quoted: true, value: tag_name };
// Transform tag name to Redrawable (reserved component name)
tag_name = 'Redrawable';
is_component = true; // Now it's a component
}
// Check for explicit self-closing syntax
if (this.match(TokenType.SELF_CLOSING)) {
const end_token = this.previous();
if (is_component) {
return createNode(NodeType.COMPONENT_INVOCATION, {
name: tag_name,
attributes,
conditionalAttributes: conditionalAttributes.length > 0 ? conditionalAttributes : undefined,
children: [],
selfClosing: true
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
else {
return createNode(NodeType.HTML_TAG, {
name: tag_name,
attributes,
conditionalAttributes: conditionalAttributes.length > 0 ? conditionalAttributes : undefined,
children: [],
selfClosing: true
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
}
// Auto-close void elements even without explicit /> syntax
// This matches standard HTML5 authoring where <input> doesn't need />
if (is_void_element) {
// Skip newlines before >
while (this.match(TokenType.NEWLINE)) {
// Skip newlines
}
const end_token = this.consume(TokenType.GT, 'Expected >');
// Void elements are always HTML tags (not components)
return createNode(NodeType.HTML_TAG, {
name: tag_name,
attributes,
conditionalAttributes: conditionalAttributes.length > 0 ? conditionalAttributes : undefined,
children: [],
selfClosing: true
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
// Must be a paired tag - skip newlines before >
while (this.match(TokenType.NEWLINE)) {
// Skip newlines
}
this.consume(TokenType.GT, 'Expected >');
// Parse children
const children = [];
// Keep parsing content until we hit the closing tag
// For $redrawable transforms, accept either original or transformed name
while (!this.check_closing_tag(tag_name) &&
!(original_tag_name && this.check_closing_tag(original_tag_name))) {
if (this.is_at_end()) {
const error = unclosedError(is_component ? 'component' : 'tag', tag_name, start_token.line, start_token.column, this.source, this.filename);
throw error;
}
const child = this.parse_content();
if (child) {
children.push(child);
}
}
// Consume closing tag
this.consume(TokenType.TAG_CLOSE, 'Expected </');
const close_name = this.consume(TokenType.TAG_NAME, 'Expected tag name');
// For $redrawable transforms, accept either original or transformed tag name
const is_valid_closing = close_name.value === tag_name ||
(original_tag_name && close_name.value === original_tag_name);
if (!is_valid_closing) {
throw mismatchedTagError(original_tag_name || tag_name, // Show original name in error
close_name.value, close_name.line, close_name.column, this.source, this.filename);
}
const end_token = this.consume(TokenType.GT, 'Expected >');
if (is_component) {
// Validate mixed content mode for components
this.validate_component_children(children, tag_name, start_token);
return createNode(NodeType.COMPONENT_INVOCATION, {
name: tag_name,
attributes,
conditionalAttributes: conditionalAttributes.length > 0 ? conditionalAttributes : undefined,
children,
selfClosing: false
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
else {
// Check if this tag needs whitespace preservation
const tag_lower = tag_name.toLowerCase();
const preserveWhitespace = tag_lower === 'textarea' || tag_lower === 'pre';
return createNode(NodeType.HTML_TAG, {
name: tag_name,
attributes,
conditionalAttributes: conditionalAttributes.length > 0 ? conditionalAttributes : undefined,
children,
selfClosing: false,
preserveWhitespace
}, start_token.start, end_token.end, start_token.line, start_token.column, this.create_location(start_token, end_token));
}
}
// Parse attributes from tokens
parse_attributes() {
const attributes = {};
const conditionalAttributes = [];
// Skip any leading newlines
while (this.match(TokenType.NEWLINE)) {
// Skip
}
while (this.check(TokenType.ATTR_NAME) || this.check(TokenType.CODE_START)) {
// Check for conditional attribute: <% if (condition) { %>
if (this.check(TokenType.CODE_START)) {
// Check if this is a closing brace <% } %> - if so, stop parsing attributes
// Peek ahead to see if the next token after CODE_START is a closing brace
const peek_next = this.tokens[this.current + 1];
if (peek_next && peek_next.type === TokenType.JAVASCRIPT && peek_next.value.trim() === '}') {
// This is a closing brace, not a new conditional attribute
// Stop parsing and return control to the caller
break;
}
const condAttr = this.parse_conditional_attribute();
if (condAttr) {
conditionalAttributes.push(condAttr);
}
// Skip newlines after conditional attribute
while (this.match(TokenType.NEWLINE)) {
// Skip
}
continue;
}
const name_token = this.advance();
let name = name_token.value;
let value = true; // Default for boolean attributes
// Check for equals sign and value
if (this.match(TokenType.EQUALS)) {
// Check if this is a compound value with interpolation
if (this.check(TokenType.ATTR_VALUE) ||
this.check(TokenType.EXPRESSION_START) ||
this.check(TokenType.EXPRESSION_UNESCAPED)) {
value = this.parse_attribute_value();
}
}
// Handle special attribute prefixes
if (name.startsWith('$')) {
// Special case: $sid becomes data-sid (needed for scoped ID system)
// All other $ attributes stay as-is (handled by instruction-processor.ts)
if (name === '$sid') {
name = 'data-sid';
}
// Keep $ prefix for other attributes - they get stored via .data() at runtime
// Keep the value object intact to preserve quoted/unquoted distinction
}
else if (name.startsWith(':')) {
// Property binding: :prop="value" becomes data-bind-prop
// Preserve whether value was quoted or not for proper code generation
name = 'data-bind-' + name.substring(1);
// Keep the value object intact to preserve quoted/unquoted distinction
}
else if (name.startsWith('@')) {
// Event binding: @click="handler" becomes data-__-on-click
// Preserve whether value was quoted or not for proper code generation
name = 'data-__-on-' + name.substring(1);
// Keep the value object intact to preserve quoted/unquoted distinction
}
attributes[name] = value;
// Skip newlines between attributes
while (this.match(TokenType.NEWLINE)) {
// Skip
}
}
return { attributes, conditionalAttributes };
}
// Parse conditional attribute: <% if (condition) { %>attr="value"<% } %>
parse_conditional_attribute() {
const start_token = this.peek();
// Consume <%
this.consume(TokenType.CODE_START, 'Expected <%');
let condition;
// Only brace style supported: CODE_START → JAVASCRIPT "if (condition) {" → TAG_END
if (this.check(TokenType.JAVASCRIPT)) {
const jsToken = this.consume(TokenType.JAVASCRIPT, 'Expected if statement');
const jsCode = jsToken.value.trim();
// Verify it starts with 'if' and contains both ( and {
if (!jsCode.startsWith('if')) {
throw syntaxError('Only if statements are allowed in attribute context. Use <% if (condition) { %>attr="value"<% } %>', jsToken.line, jsToken.column, this.source);
}
// Extract condition from: if (condition) {
const openParen = jsCode.indexOf('(');
const closeBrace = jsCode.lastIndexOf('{');
if (openParen === -1 || closeBrace === -1) {
throw syntaxError('Expected format: <% if (condition) { %>', jsToken.line, jsToken.column, this.source);
}
// Extract just the condition part (between parens, including parens)
condition = jsCode.substring(openParen, closeBrace).trim();
}
else {
// Not an if statement
throw syntaxError('Only if statements are allowed in attribute context. Use <% if (condition) { %>attr="value"<% } %>', this.peek().line, this.peek().column, this.source);
}
// Consume %>
this.consume(TokenType.TAG_END, 'Expected %>');
// Now parse the attributes inside the conditional
const innerAttrs = this.parse_attributes();
// These should be plain attributes only (no nested conditionals)
if (innerAttrs.conditionalAttributes.length > 0) {
throw syntaxError('Nested conditional attributes are not supported', start_token.line, start_token.column, this.source);
}
// Consume <% } %>
this.consume(TokenType.CODE_START, 'Expected <% to close conditional attribute');
const closeToken = this.consume(TokenType.JAVASCRIPT, 'Expected }');
if (closeToken.value.trim() !== '}') {
throw syntaxError('Expected } to close if statement', closeToken.line, closeToken.column, this.source);
}
this.consume(TokenType.TAG_END, 'Expected %>');
return createNode(NodeType.CONDITIONAL_ATTRIBUTE, {
condition,
attributes: innerAttrs.attributes
}, start_token.start, this.previous().end, start_token.line, start_token.column);
}
// Parse potentially compound attribute value
parse_attribute_value() {
const parts = [];
// For simple string values that are quoted in the source, return them with a quoted flag
// This helps the codegen distinguish between $foo="bar" and $foo=bar
const firstToken = this.peek();
const isSimpleValue = this.check(TokenType.ATTR_VALUE) &&
!this.check_ahead(1, TokenType.EXPRESSION_START) &&
!this.check_ahead(1, TokenType.EXPRESSION_UNESCAPED);
// Collect all parts of the attribute value
while (this.check(TokenType.ATTR_VALUE) ||
this.check(TokenType.EXPRESSION_START) ||
this.check(TokenType.EXPRESSION_UNESCAPED)) {
if (this.check(TokenType.ATTR_VALUE)) {
const token = this.advance();
// Trim whitespace from attribute value text parts to avoid extra newlines
const trimmedValue = token.value.trim();
if (trimmedValue.length > 0) {
parts.push({ type: 'text', value: trimmedValue, escaped: true });
}
}
else if (this.check(TokenType.EXPRESSION_START) ||
this.check(TokenType.EXPRESSION_UNESCAPED)) {
const is_escaped = this.peek().type === TokenType.EXPRESSION_START;
this.advance(); // consume <%= or <%!=
const expr_token = this.consume(TokenType.JAVASCRIPT, 'Expected expression');
this.consume(TokenType.TAG_END, 'Expected %>');
parts.push({ type: 'expression', value: expr_token.value, escaped: is_escaped });
}
}
// If it's a single text part, check if it's quoted
if (parts.length === 1 && parts[0].type === 'text') {
const value = parts[0].value;
// Check if the value has quotes (preserved by lexer for quoted strings)
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
// Return a marker that this was a quoted string
return { quoted: true, value: value.slice(1, -1) };
}
// Check if it's a parenthesized expression: $attr=(expr)
if (value.startsWith('(') && value.endsWith(')')) {
// Return as an expression - remove the parentheses
return { expression: true, value: value.slice(1, -1) };
}
// Check if it's a bare identifier or member expression: $attr=identifier or $attr=this.method
// Valid identifiers can include dots for member access (e.g., this.handleClick, data.user.name)
// Can be prefixed with ! for negation (e.g., !this.canEdit)
// Pattern: optional ! then starts with letter/$/_ then any combo of letters/numbers/$/_ and dots
if (/^!?[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(value)) {
// Return as an identifier expression
return { identifier: true, value: value };
}
// Check if it contains function calls (parentheses)
// If the lexer allowed it (passed validation), treat it as an identifier/expression
// Examples: getData(), obj.method(arg), rsx.route('A','B').url(123)
if (value.includes('(') || value.includes(')')) {
// Return as an identifier expression (function call chain)
return { identifier: true, value: value };
}
// Otherwise, treat as a JavaScript expression (includes numeric literals like 42, 3.14, etc.)
return { expression: true, value: value };
}
// Any expression or multiple parts needs interpolation handling
return { interpolated: true, parts };
}
// Check if we're at a closing tag for the given name
check_closing_tag(tag_name) {
if (!this.check(TokenType.TAG_CLOSE)) {
return false;
}
// Look ahead to see if the tag name matches
const next_pos = this.current + 1;
if (next_pos < this.tokens.length &&
this.tokens[next_pos].type === TokenType.TAG_NAME &&
this.tokens[next_pos].value === tag_name) {
return true;
}
return false;
}
match(...types) {
for (const type of types) {
if (this.check(type)) {
this.advance();
return true;
}
}
return false;
}
check(type) {
if (this.is_at_end())
return false;
return this.peek().type === type;
}
check_ahead(offset, type) {
if (this.current + offset >= this.tokens.length) {
return false;
}
return this.tokens[this.current + offset].type === type;
}
check_sequence(...types) {
for (let i = 0; i < types.length; i++) {
if (this.current + i >= this.tokens.length) {
return false;
}
if (this.tokens[this.current + i].type !== types[i]) {
return false;
}
}
return true;
}
advance() {
if (!this.is_at_end())
this.current++;
return this.previous();
}
is_at_end() {
return this.peek().type === TokenType.EOF;
}
peek() {
return this.tokens[this.current];
}
peek_ahead(offset) {
const pos = this.current + offset;
if (pos >= this.tokens.length) {
return this.tokens[this.tokens.length - 1]; // Return EOF token
}
return this.tokens[pos];
}
previous() {
return this.tokens[this.current - 1];
}
current_token() {
return this.tokens[this.current] || this.tokens[this.tokens.length - 1];
}
previous_token() {
return this.tokens[Math.max(0, this.current - 1)];
}
/**
* Create a SourceLocation from start and end tokens
* Propagates loc field if available, falls back to old fields for compatibility
*/
create_location(start, end) {
if (start.loc && end.loc) {
// Use new loc field if available
return {
start: start.loc.start,
end: end.loc.end
};
}
// Fall back to old fields for backward compatibility
return undefined;
}
consume(type, message) {
if (this.check(type))
return this.advance();
const token = this.peek();
// Special case: Detecting template expressions inside HTML tag attributes
if (type === TokenType.GT &&
(token.type === TokenType.EXPRESSION_START || token.type === TokenType.EXPRESSION_UNESCAPED)) {
const error = syntaxError('Template expressions (<% %>) cannot be used as attribute values inside HTML tags', token.line, token.column, this.source, this.filename);
// Add helpful remediation examples
error.message += '\n\n' +
' Use template expressions INSIDE attribute values instead:\n' +
' ✅ <tag style="<%= expression %>">\n' +
' ✅ <tag class="<%= condition ? \'active\' : \'\' %>">\n\n' +
' Or use conditional logic before the tag:\n' +
' ✅ <% let attrs = expression ? \'value\' : \'\'; %>\n' +
' <tag attr="<%= attrs %>">\n\n' +
' Or set attributes in on_ready() using jQuery:\n' +
' ✅ <tag $sid="my_element">\n' +
' on_ready() {\n' +
' if (this.args.required) this.$sid(\'my_element\').attr(\'required\', true);\n' +
' }';
throw error;
}
const error = syntaxError(`${message}. Got ${token.type} instead`, token.line, token.column, this.source, this.filename);
throw error;
}
// Validate component children to prevent mixed content mode
validate_component_children(children, componentName, startToken) {
let hasSlots = false;
let hasNonSlotContent = false;
for (const child of children) {
if (child.type === NodeType.SLOT) {
hasSlots = true;
}
else if (child.type === NodeType.TEXT) {
// Check if it's non-whitespace text
const textContent = child.content;
if (textContent.trim() !== '') {
hasNonSlotContent = true;
}
}
else {
// Any other node type (expressions, tags, etc.) is non-slot content
hasNonSlotContent = true;
}
}
// If component has both slots and non-slot content, throw error
if (hasSlots && hasNonSlotContent) {
throw syntaxError(`Mixed content not allowed: when using slots, all content must be inside <Slot:slotname> tags`, startToken.line, startToken.column, this.source, this.filename);
}
}
/**
* Compile method for simplified API
* Parses the template and returns component metadata and render function
*/
compile() {
// Parse to get AST
const ast = this.parse();
// Generate code with sourcemap
const generator = new CodeGenerator();
const result = generator.generateWithSourceMap(ast, this.filename || 'template.jqhtml', this.source || '');
// Extract the single component (should only be one per file)
const componentEntries = Array.from(result.components.entries());
if (componentEntries.length === 0) {
throw new Error('No component definition found in template');
}
if (componentEntries.length > 1) {
const names = componentEntries.map(([name]) => name).join(', ');
throw new Error(`Multiple component definitions found: ${names}. Only one component per file is allowed.`);
}
// Extract component information
const [name, componentDef] = componentEntries[0];
return {
name: name,
tagName: componentDef.tagName || 'div',
defaultAttributes: componentDef.defaultAttributes || {},
renderFunction: componentDef.render_function
};
}
}
//# sourceMappingURL=parser.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
export declare function process_instructions(instructions: any[], component: any): JQuery;
export declare function html(str: any): string;
export declare function register_template(name: string, render_fn: Function): void;
export declare function get_template(name: string): Function | undefined;
export declare function create_templated_component(ComponentClass: any, template_name: string, args?: Record<string, any>, element?: JQuery): any;
//# sourceMappingURL=runtime.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAMA,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,GAAG,MAAM,CA6EhF;AAGD,wBAAgB,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAMrC;AAMD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,QAElE;AAGD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAE/D;AAGD,wBAAgB,0BAA0B,CACxC,cAAc,EAAE,GAAG,EACnB,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EAC9B,OAAO,CAAC,EAAE,MAAM,GACf,GAAG,CAsBL"}

View File

@@ -0,0 +1,106 @@
// JQHTML v2 Runtime - Processes instruction arrays from compiled templates
// Minimal implementation focused on MVP functionality
/// <reference path="./types.d.ts" />
// Process instruction array into DOM elements
export function process_instructions(instructions, component) {
const fragment = document.createDocumentFragment();
const stack = [];
let current = fragment;
for (const instruction of instructions) {
// Plain text
if (typeof instruction === 'string') {
const text = document.createTextNode(instruction);
current.appendChild(text);
continue;
}
// Tag instruction: {tag: [name, attrs, selfClosing]}
if (instruction.tag) {
const [tag_name, attrs, self_closing] = instruction.tag;
const element = document.createElement(tag_name);
// Set attributes
for (const [key, value] of Object.entries(attrs || {})) {
element.setAttribute(key, String(value));
}
current.appendChild(element);
// Push to stack if not self-closing
if (!self_closing) {
stack.push(current);
current = element;
}
continue;
}
// Component instruction: {comp: [name, props]}
if (instruction.comp) {
const [comp_name, props] = instruction.comp;
// Create placeholder div for component
const placeholder = document.createElement('div');
placeholder.setAttribute('data-component', comp_name);
placeholder.setAttribute('data-props', JSON.stringify(props || {}));
placeholder.setAttribute('data-state', 'uninitialized');
placeholder.className = 'jqhtml-component';
current.appendChild(placeholder);
// TODO: In future, instantiate component here
continue;
}
// Slot instruction: {slot: [name, props, renderFn]}
if (instruction.slot) {
const [slot_name, props, render_fn] = instruction.slot;
// Execute slot render function if provided
if (render_fn && typeof render_fn === 'function') {
const slot_context = { ...props };
const [slot_output] = render_fn.call(component, slot_context);
// Process slot output recursively
const slot_dom = process_instructions(slot_output, component);
slot_dom.each(function () {
current.appendChild(this);
});
}
continue;
}
// Closing tag (when we see a string like "</div>")
if (typeof instruction === 'string' && instruction.match(/^<\//)) {
if (stack.length > 0) {
current = stack.pop();
}
}
}
return $(fragment.childNodes);
}
// HTML escape function required by generated code
export function html(str) {
if (str == null)
return '';
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
// Component template registry
const template_registry = new Map();
// Register a compiled template
export function register_template(name, render_fn) {
template_registry.set(name, render_fn);
}
// Get a compiled template
export function get_template(name) {
return template_registry.get(name);
}
// Create component with template
export function create_templated_component(ComponentClass, template_name, args = {}, element) {
const template = get_template(template_name);
if (!template) {
throw new Error(`Template not found: ${template_name}`);
}
// Create component instance - element first, then args
const component = new ComponentClass(element, args);
// Override on_render to use template
component.on_render = async function () {
// Call template render function
const [instructions] = template.call(this, ComponentClass, this.data, this.args, {});
// Process instructions into DOM
const rendered = process_instructions(instructions, this);
// Clear and append
this.$.empty().append(rendered);
};
return component;
}
//# sourceMappingURL=runtime.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,55 @@
{
"name": "@jqhtml/parser",
"version": "2.3.7",
"description": "JQHTML template parser - converts templates to JavaScript",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"jqhtml-compile": "./bin/jqhtml-compile"
},
"scripts": {
"build": "tsc && node ../../scripts/inject-parser-version.js",
"build:no-inject": "tsc",
"test": "jest",
"clean": "rm -rf dist",
"baseline:generate": "node test-regression/generate-baseline.js",
"test:regression": "node test-regression/compare-outputs.js",
"test:regression:verbose": "node test-regression/compare-outputs.js --verbose",
"validate:sourcemap": "node test-regression/validate-sourcemaps.js"
},
"browser": false,
"engines": {
"node": ">=14.0.0"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"bin",
"README.md",
"LICENSE"
],
"keywords": [
"jqhtml",
"parser",
"template",
"compiler",
"jquery"
],
"author": "JQHTML Team",
"license": "MIT",
"dependencies": {
"@types/jest": "^29.5.11",
"@types/jquery": "^3.5.32",
"@types/node": "^20.10.5",
"jest": "^29.7.0",
"source-map": "^0.7.4",
"typescript": "^5.3.3"
},
"devDependencies": {
"chalk": "^5.6.2",
"diff": "^8.0.2"
}
}