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:
147
node_modules/@jqhtml/parser/dist/parser.js
generated
vendored
147
node_modules/@jqhtml/parser/dist/parser.js
generated
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user