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

@@ -10,6 +10,7 @@ export class CodeGenerator {
current_component = null;
in_slot = false;
tag_depth = 0;
lastOutput = ''; // Track last generated output for deduplication
// Position tracking for source maps
outputLine = 1;
outputColumn = 0;
@@ -178,6 +179,7 @@ export class CodeGenerator {
}
generate_component(node) {
this.current_component = node.name;
this.lastOutput = ''; // Reset output tracking for each component
const dependencies = new Set();
// Always use 1:1 line mapping for proper sourcemaps
// Even when using SourceMapGenerator, we need the line structure
@@ -234,8 +236,11 @@ export class CodeGenerator {
slots[slotNode.name] = slotNode;
}
}
// Filter out TEXT nodes (whitespace) from slot-only templates
// TEXT nodes cause _output.push() calls which are invalid in slot-only context
const slotsOnly = node.body.filter(child => child.type === NodeType.SLOT);
// Use 1:1 line mapping for slot-only templates
const bodyLines = this.generate_function_body_1to1(node.body);
const bodyLines = this.generate_function_body_1to1(slotsOnly);
// Build the render function with line preservation
const lines = [];
// Line 1: function declaration returning slots object
@@ -387,39 +392,89 @@ export class CodeGenerator {
// Generate code based on node type
switch (node.type) {
case NodeType.HTML_TAG: {
this.lastOutput = ''; // Reset for non-text output
const tag = node;
// Opening tag goes on its line
const openTag = this.generate_tag_open(tag);
if (openTag) {
lines[lineIndex] = (lines[lineIndex] || '') + openTag;
// Check if this is a raw content tag (textarea, pre)
if (tag.preserveWhitespace && !tag.selfClosing && tag.children && tag.children.length > 0) {
// Validate: only TEXT, EXPRESSION, and CODE_BLOCK children allowed in raw content tags
// HTML tags and components are not allowed (they would break whitespace preservation)
for (const child of tag.children) {
if (child.type !== NodeType.TEXT &&
child.type !== NodeType.EXPRESSION &&
child.type !== NodeType.CODE_BLOCK) {
const error = new JQHTMLParseError(`Invalid content in <${tag.name}> tag`, tag.line, tag.column || 0, this.sourceContent, this.sourceFile);
error.suggestion =
`\n\nAll content within <textarea> and <pre> tags must be plain text or expressions.\n` +
`HTML tags and components are not allowed.\n\n` +
`Allowed:\n` +
` <textarea><%= this.data.value %></textarea> ← expressions OK\n` +
` <textarea>plain text</textarea> ← plain text OK\n\n` +
`Not allowed:\n` +
` <textarea><div>content</div></textarea> ← HTML tags not OK\n` +
` <textarea><MyComponent /></textarea> ← components not OK\n\n` +
`This ensures proper whitespace preservation.`;
throw error;
}
}
// Generate rawtag instruction with raw content
const attrs_obj = this.generate_attributes_with_conditionals(tag.attributes, tag.conditionalAttributes);
// Collect raw content from children (all validated as TEXT)
let rawContent = '';
for (const child of tag.children) {
rawContent += child.content;
}
// Escape the raw content for JavaScript string
const escapedContent = this.escape_string(rawContent);
const rawtagInstruction = `_output.push({rawtag: ["${tag.name}", ${attrs_obj}, ${escapedContent}]});`;
lines[lineIndex] = (lines[lineIndex] || '') + rawtagInstruction;
}
// Process children
if (tag.children) {
tag.children.forEach(processNodeForLine);
}
// Closing tag might be on a different line
const closeTag = `_output.push("</${tag.name}>");`;
// For simplicity, put closing tag on the last child's line or same line
const closeLine = tag.children && tag.children.length > 0
? (tag.children[tag.children.length - 1].line || node.line)
: node.line;
const closeIndex = closeLine - 2;
if (closeIndex >= 0 && closeIndex < lines.length) {
lines[closeIndex] = (lines[closeIndex] || '') + ' ' + closeTag;
else {
// Normal HTML tag processing
// Opening tag goes on its line
const openTag = this.generate_tag_open(tag);
if (openTag) {
lines[lineIndex] = (lines[lineIndex] || '') + openTag;
}
// Process children
if (tag.children) {
tag.children.forEach(processNodeForLine);
}
// Closing tag might be on a different line
const closeTag = `_output.push("</${tag.name}>");`;
// For simplicity, put closing tag on the last child's line or same line
const closeLine = tag.children && tag.children.length > 0
? (tag.children[tag.children.length - 1].line || node.line)
: node.line;
const closeIndex = closeLine - 2;
if (closeIndex >= 0 && closeIndex < lines.length) {
lines[closeIndex] = (lines[closeIndex] || '') + ' ' + closeTag;
}
}
break;
}
case NodeType.TEXT: {
const text = node;
// Only generate code for non-whitespace text
if (text.content.trim()) {
const code = `_output.push(${this.escape_string(text.content)});`;
// Apply padded trim to preserve intentional whitespace
const processed = this.padded_trim(text.content);
if (processed) {
const code = `_output.push(${this.escape_string(processed)});`;
// Optimization: skip consecutive identical space pushes
if (code === '_output.push(" ");' && this.lastOutput === '_output.push(" ");') {
// Skip duplicate - don't add to output
break;
}
this.lastOutput = code; // Track for next comparison
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
}
// Whitespace-only text nodes don't generate code but preserve line positioning
else {
// Empty text resets tracking so next space won't be skipped
this.lastOutput = '';
}
// Empty after processing: skip (no code generated)
break;
}
case NodeType.CODE_BLOCK: {
this.lastOutput = ''; // Reset for non-text output
const codeBlock = node;
const code = this.generate_code_block(codeBlock);
if (code) {
@@ -440,11 +495,16 @@ export class CodeGenerator {
break;
}
case NodeType.EXPRESSION: {
this.lastOutput = ''; // Reset for non-text output
const expr = node;
// Generate the expression wrapper on a single line
let code;
// Special handling for content() calls
const trimmedCode = expr.code.trim();
// Strip trailing semicolon if present (optional in <%= %> blocks)
let trimmedCode = expr.code.trim();
if (trimmedCode.endsWith(';')) {
trimmedCode = trimmedCode.slice(0, -1).trim();
}
if (trimmedCode === 'content()') {
// Default slot/content - check _inner_html first
code = `(() => { if (this.args._inner_html) { _output.push(this.args._inner_html); } else if (typeof content === 'function') { const [contentInstructions] = content.call(this); _output.push(['_content', contentInstructions]); } })();`;
@@ -456,17 +516,8 @@ export class CodeGenerator {
}
else if (trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/)) {
// Named slot: content('header') or content('header', data) (function call with string parameter and optional data)
const match = trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/);
const slotName = match[1];
const dataParam = match[2];
if (dataParam) {
// With data parameter: content('row', record) -> pass data to slot function
code = `(() => { if (typeof content === 'object' && typeof content.${slotName} === 'function') { const [contentInstructions] = content.${slotName}.call(this, ${dataParam}); _output.push(['_content', contentInstructions]); } })();`;
}
else {
// Without data parameter: content('header') -> pass undefined
code = `(() => { if (typeof content === 'object' && typeof content.${slotName} === 'function') { const [contentInstructions] = content.${slotName}.call(this); _output.push(['_content', contentInstructions]); } })();`;
}
// Use the standard result pattern for proper handling
code = `(() => { const result = ${trimmedCode};; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(jqhtml.escape_html(result)); } })();`;
}
else if (expr.escaped) {
code = `(() => { const result = ${expr.code}; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(jqhtml.escape_html(result)); } })();`;
@@ -481,9 +532,10 @@ export class CodeGenerator {
break;
}
case NodeType.COMPONENT_INVOCATION: {
this.lastOutput = ''; // Reset for non-text output
const comp = node;
// For 1:1 mapping, generate compact component invocations
const attrs = this.generate_attributes_object(comp.attributes);
const attrs = this.generate_attributes_with_conditionals(comp.attributes, comp.conditionalAttributes);
if (comp.selfClosing || comp.children.length === 0) {
// Simple component without children
const code = `_output.push({comp: ["${comp.name}", ${attrs}]});`;
@@ -621,15 +673,44 @@ export class CodeGenerator {
return '';
}
}
/**
* Padded trim: Collapse internal whitespace but preserve leading/trailing space
* Examples:
* " hello " → " hello "
* "hello" → "hello"
* " " → " "
* "\n\n \n" → " "
*/
padded_trim(text) {
const has_leading_space = /^\s/.test(text);
const has_trailing_space = /\s$/.test(text);
// Trim the text
let result = text.trim();
// Add back single space if original had leading/trailing whitespace
if (has_leading_space)
result = ' ' + result;
if (has_trailing_space)
result = result + ' ';
// Final pass: collapse all whitespace sequences to single space
return result.replace(/\s+/g, ' ');
}
generate_text(node) {
const content = node.content;
// Skip empty text nodes (just whitespace)
if (content.trim() === '') {
// Apply padded trim to preserve intentional whitespace
const processed = this.padded_trim(content);
// Skip if empty after processing
if (!processed) {
return '';
}
// Plain text - escape for JavaScript string
const escaped = this.escape_string(content);
// Generate output code
const escaped = this.escape_string(processed);
const output = `_output.push(${escaped});`;
// Optimization: skip consecutive identical space pushes (but never skip newlines)
if (output === '_output.push(" ");' && this.lastOutput === '_output.push(" ");') {
return ''; // Skip duplicate space push
}
// Track this output for next comparison
this.lastOutput = output;
// Track the emitted position with source mapping
if (this.enablePositionTracking) {
this.emit(output, node);
@@ -637,9 +718,14 @@ export class CodeGenerator {
return output;
}
generate_expression(node) {
this.lastOutput = ''; // Reset for non-text output
let output;
// Special handling for content() calls
const trimmedCode = node.code.trim();
// Strip trailing semicolon if present (optional in <%= %> blocks)
let trimmedCode = node.code.trim();
if (trimmedCode.endsWith(';')) {
trimmedCode = trimmedCode.slice(0, -1).trim();
}
if (trimmedCode === 'content()') {
// Default slot/content - check _inner_html first
output = `(() => { if (this.args._inner_html) { _output.push(this.args._inner_html); } else if (typeof content === 'function') { const [contentInstructions] = content.call(this); _output.push(['_content', contentInstructions]); } })();`;
@@ -651,17 +737,8 @@ export class CodeGenerator {
}
else if (trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/)) {
// Named slot: content('header') or content('header', data) (function call with string parameter and optional data)
const match = trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/);
const slotName = match[1];
const dataParam = match[2];
if (dataParam) {
// With data parameter: content('row', record) -> pass data to slot function
output = `(() => { if (typeof content === 'object' && typeof content.${slotName} === 'function') { const [contentInstructions] = content.${slotName}.call(this, ${dataParam}); _output.push(['_content', contentInstructions]); } })();`;
}
else {
// Without data parameter: content('header') -> pass undefined
output = `(() => { if (typeof content === 'object' && typeof content.${slotName} === 'function') { const [contentInstructions] = content.${slotName}.call(this); _output.push(['_content', contentInstructions]); } })();`;
}
// Use the standard result pattern for proper handling
output = `(() => { const result = ${trimmedCode};; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(jqhtml.escape_html(result)); } })();`;
}
else if (node.escaped) {
// Single-line expression handler for escaped output
@@ -819,15 +896,48 @@ export class CodeGenerator {
}
generate_tag_open(node) {
// Generate just the opening tag
const attrs = this.generate_attributes_object(node.attributes);
const attrs = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
return `_output.push({tag: ["${node.name}", ${attrs}, ${node.selfClosing || false}]});`;
}
generate_html_tag(node) {
// Generate opening tag, children, and closing tag on same line
// This keeps related content together
this.lastOutput = ''; // Reset for non-text output
// Check if this tag needs raw content preservation
if (node.preserveWhitespace && !node.selfClosing && node.children.length > 0) {
// Validate: only TEXT, EXPRESSION, and CODE_BLOCK children allowed in raw content tags
// HTML tags and components are not allowed (they would break whitespace preservation)
for (const child of node.children) {
if (child.type !== NodeType.TEXT &&
child.type !== NodeType.EXPRESSION &&
child.type !== NodeType.CODE_BLOCK) {
const error = new JQHTMLParseError(`Invalid content in <${node.name}> tag`, node.line, node.column || 0, this.sourceContent, this.sourceFile);
error.suggestion =
`\n\nAll content within <textarea> and <pre> tags must be plain text or expressions.\n` +
`HTML tags and components are not allowed.\n\n` +
`Allowed:\n` +
` <textarea><%= this.data.value %></textarea> ← expressions OK\n` +
` <textarea>plain text</textarea> ← plain text OK\n\n` +
`Not allowed:\n` +
` <textarea><div>content</div></textarea> ← HTML tags not OK\n` +
` <textarea><MyComponent /></textarea> ← components not OK\n\n` +
`This ensures proper whitespace preservation.`;
throw error;
}
}
// Generate rawtag instruction with raw content
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
// Collect raw content from children (all validated as TEXT)
let rawContent = '';
for (const child of node.children) {
rawContent += child.content;
}
// Escape the raw content for JavaScript string
const escapedContent = this.escape_string(rawContent);
return `_output.push({rawtag: ["${node.name}", ${attrs_obj}, ${escapedContent}]});`;
}
// Normal tag generation
const parts = [];
// Generate opening tag instruction
const attrs_obj = this.generate_attributes_object(node.attributes);
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
parts.push(`_output.push({tag: ["${node.name}", ${attrs_obj}, ${node.selfClosing}]});`);
if (!node.selfClosing) {
// Generate children inline
@@ -849,8 +959,9 @@ export class CodeGenerator {
return parts.join(' ');
}
generate_component_invocation(node) {
this.lastOutput = ''; // Reset for non-text output
const instructions = [];
const attrs_obj = this.generate_attributes_object(node.attributes);
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
if (node.selfClosing || node.children.length === 0) {
// Simple component without children
const componentCall = `_output.push({comp: ["${node.name}", ${attrs_obj}]});`;
@@ -902,6 +1013,23 @@ export class CodeGenerator {
}
return attrs;
}
// Generate attribute object including conditional attributes
generate_attributes_with_conditionals(attrs, conditionalAttrs) {
// If no conditional attributes, use simple object
if (!conditionalAttrs || conditionalAttrs.length === 0) {
return this.generate_attributes_object(attrs);
}
// We have conditional attributes - need to merge them at runtime
const baseAttrs = this.generate_attributes_object(attrs);
// Generate code that conditionally adds attributes
let result = baseAttrs;
for (const condAttr of conditionalAttrs) {
const condAttrsObj = this.generate_attributes_object(condAttr.attributes);
// Use Object.assign to merge conditional attributes
result = `Object.assign({}, ${result}, (${condAttr.condition}) ? ${condAttrsObj} : {})`;
}
return result;
}
generate_attributes_object(attrs) {
if (Object.keys(attrs).length === 0) {
return '{}';
@@ -909,11 +1037,14 @@ export class CodeGenerator {
const entries = Object.entries(attrs).flatMap(([key, value]) => {
// Convert 'tag' to '_tag' for component invocations
const attrKey = key === 'tag' ? '_tag' : key;
// Special handling for id attribute - append _cid for scoping
if (key === 'id') {
// Special handling for data-id attribute (from $id) - create scoped id
// NOTE: Parser converts $id="foo" → data-id="foo" so we can distinguish from regular id
// This generates: id="foo:PARENT_CID" data-id="foo"
// The :PARENT_CID scoping happens at runtime in instruction-processor.ts
if (key === 'data-id') {
const id_entries = [];
if (value && typeof value === 'object' && value.interpolated) {
// Interpolated ID like id="user<%= index %>"
// Interpolated $id like $id="user<%= index %>"
const parts = value.parts.map((part) => {
if (part.type === 'text') {
return this.escape_string(part.value);
@@ -927,19 +1058,29 @@ export class CodeGenerator {
id_entries.push(`"data-id": ${base_id}`);
}
else if (value && typeof value === 'object' && value.quoted) {
// Quoted ID like $id="static"
// Quoted $id like $id="static"
const base_id = this.escape_string(value.value);
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-id": ${base_id}`);
}
else {
// Simple ID like id="username" or expression like $id=someVar
// Simple $id like $id="username" or expression like $id=someVar
const base_id = this.escape_string(String(value));
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-id": ${base_id}`);
}
return id_entries;
}
// Regular id attribute - pass through unchanged
// id="foo" remains id="foo" (no scoping)
if (key === 'id') {
if (value && typeof value === 'object' && value.quoted) {
return `"id": ${this.escape_string(value.value)}`;
}
else {
return `"id": ${this.escape_string(String(value))}`;
}
}
// Check if this is an interpolated attribute value
if (value && typeof value === 'object' && value.interpolated) {
// Build concatenation expression
@@ -948,8 +1089,9 @@ export class CodeGenerator {
return this.escape_string(part.value);
}
else {
// Expression - no escaping in attributes
return part.value;
// Expression - wrap in parentheses to preserve operator precedence
// This ensures "a" + (x ? 'b' : 'c') instead of "a" + x ? 'b' : 'c'
return `(${part.value})`;
}
});
return `"${attrKey}": ${parts.join(' + ')}`;
@@ -1201,7 +1343,7 @@ export class CodeGenerator {
for (const [name, component] of this.components) {
code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${name}', {\n`;
code += ` _jqhtml_version: '2.2.142',\n`; // Version will be replaced during build
code += ` _jqhtml_version: '2.2.171',\n`; // Version will be replaced during build
code += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;