Enhance refactor commands with controller-aware Route() updates and fix code quality violations

Add semantic token highlighting for 'that' variable and comment file references in VS Code extension
Add Phone_Text_Input and Currency_Input components with formatting utilities
Implement client widgets, form standardization, and soft delete functionality
Add modal scroll lock and update documentation
Implement comprehensive modal system with form integration and validation
Fix modal component instantiation using jQuery plugin API
Implement modal system with responsive sizing, queuing, and validation support
Implement form submission with validation, error handling, and loading states
Implement country/state selectors with dynamic data loading and Bootstrap styling
Revert Rsx::Route() highlighting in Blade/PHP files
Target specific PHP scopes for Rsx::Route() highlighting in Blade
Expand injection selector for Rsx::Route() highlighting
Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls
Update jqhtml packages to v2.2.165
Add bundle path validation for common mistakes (development mode only)
Create Ajax_Select_Input widget and Rsx_Reference_Data controller
Create Country_Select_Input widget with default country support
Initialize Tom Select on Select_Input widgets
Add Tom Select bundle for enhanced select dropdowns
Implement ISO 3166 geographic data system for country/region selection
Implement widget-based form system with disabled state support

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-30 06:21:56 +00:00
parent e678b987c2
commit f6ac36c632
5683 changed files with 5854736 additions and 22329 deletions

View File

@@ -2,7 +2,7 @@
// Simple recursive descent parser, no complex libraries
import { TokenType } from './lexer.js';
import { NodeType, createNode } from './ast.js';
import { unclosedError, mismatchedTagError, syntaxError, getSuggestion } from './errors.js';
import { JQHTMLParseError, unclosedError, mismatchedTagError, syntaxError, getSuggestion } from './errors.js';
import { CodeGenerator } from './codegen.js';
export class Parser {
tokens;
@@ -20,6 +20,28 @@ export class Parser {
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 = [];
@@ -252,6 +274,8 @@ export class Parser {
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,
@@ -268,6 +292,10 @@ export class Parser {
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 %>');
@@ -461,7 +489,7 @@ export class Parser {
// 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 = this.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) {
@@ -471,7 +499,7 @@ export class Parser {
// Store original tag name for closing tag matching
original_tag_name = tag_name;
// Add tag="original_tag_name" attribute
attributes['data-tag'] = { quoted: true, value: tag_name };
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
@@ -483,6 +511,7 @@ export class Parser {
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));
@@ -491,6 +520,7 @@ export class Parser {
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));
@@ -508,6 +538,7 @@ export class Parser {
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));
@@ -549,27 +580,54 @@ export class Parser {
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
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)) {
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
@@ -584,15 +642,11 @@ export class Parser {
}
// Handle special attribute prefixes
if (name.startsWith('$')) {
if (name === '$id') {
// Special case: $id becomes regular id (will be scoped in codegen)
name = 'id';
}
else {
// General case: $property becomes data-property
name = 'data-' + name.substring(1);
// Keep the value object intact to preserve quoted/unquoted distinction
}
// General case: $property becomes data-property
// This includes $id → data-id (for scoped IDs)
// The distinction between data-id (scoped) and id (pass-through) is preserved
name = 'data-' + name.substring(1);
// Keep the value object intact to preserve quoted/unquoted distinction
}
else if (name.startsWith(':')) {
// Property binding: :prop="value" becomes data-bind-prop
@@ -612,7 +666,63 @@ export class Parser {
// Skip
}
}
return attributes;
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;
// Two possibilities:
// 1. Brace style: CODE_START → JAVASCRIPT "if (condition) {" → TAG_END
// 2. Colon style: CODE_START → IF → JAVASCRIPT "(condition)" → TAG_END
if (this.check(TokenType.JAVASCRIPT)) {
// Brace style
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 if (this.check(TokenType.IF)) {
// Colon style
this.advance(); // consume 'if'
const jsToken = this.consume(TokenType.JAVASCRIPT, 'Expected condition after if');
condition = jsToken.value.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() {
@@ -662,6 +772,13 @@ export class Parser {
// 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 literal string value
return value;
}